-
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add PowerShell command to publish a Microsoft Store app to Intune and added some unit tests.
- Loading branch information
Showing
20 changed files
with
586 additions
and
295 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
src/Svrooij.WinTuner.CmdLets/Commands/DeployWtMsStoreApp.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
using Microsoft.Extensions.Logging; | ||
using Svrooij.PowerShell.DependencyInjection; | ||
using System; | ||
using System.Management.Automation; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using WingetIntune.Graph; | ||
using GraphModels = Microsoft.Graph.Beta.Models; | ||
|
||
namespace Svrooij.WinTuner.CmdLets.Commands; | ||
/// <summary> | ||
/// <para type="synopsis">Create a MsStore app in Intune</para> | ||
/// <para type="description">Use this command to create an Microsoft Store app in Microsoft Intune</para> | ||
/// <para type="link" uri="https://wintuner.app/docs/wintuner-powershell/Deploy-WtMsStoreApp">Documentation</para> | ||
/// </summary> | ||
/// <example> | ||
/// <para type="description">Add Firefox to Intune, using interactive authentication</para> | ||
/// <code>Deploy-WtMsStoreApp -PackageId 9NZVDKPMR9RD -Username [email protected]</code> | ||
/// </example> | ||
[Cmdlet(VerbsLifecycle.Deploy, "WtMsStoreApp", DefaultParameterSetName = nameof(PackageId))] | ||
[OutputType(typeof(GraphModels.WinGetApp))] | ||
public class DeployWtMsStoreApp : BaseIntuneCmdlet | ||
{ | ||
/// <summary> | ||
/// <para type="description">The package id to upload to Intune.</para> | ||
/// </summary> | ||
[Parameter( | ||
Mandatory = true, | ||
Position = 0, | ||
ParameterSetName = nameof(PackageId), | ||
ValueFromPipeline = false, | ||
ValueFromPipelineByPropertyName = false, | ||
HelpMessage = "The package id to upload to Intune.")] | ||
public string? PackageId { get; set; } | ||
|
||
/// <summary> | ||
/// <para type="description">Name of the app to look for, first match will be created.</para> | ||
/// </summary> | ||
[Parameter( | ||
Mandatory = true, | ||
Position = 0, | ||
ParameterSetName = nameof(SearchQuery), | ||
ValueFromPipeline = false, | ||
ValueFromPipelineByPropertyName = false, | ||
HelpMessage = "Name of the app to look for, first match will be created.")] | ||
public string? SearchQuery { get; set; } | ||
|
||
[ServiceDependency] | ||
private ILogger<DeployWtMsStoreApp>? logger; | ||
|
||
[ServiceDependency] | ||
private GraphStoreAppUploader? graphStoreAppUploader; | ||
|
||
[ServiceDependency] | ||
private HttpClient? httpClient; | ||
|
||
/// <inheritdoc/> | ||
public override async Task ProcessRecordAsync(CancellationToken cancellationToken) | ||
{ | ||
ValidateAuthenticationParameters(); | ||
if (ParameterSetName == nameof(SearchQuery)) | ||
{ | ||
ArgumentException.ThrowIfNullOrWhiteSpace(SearchQuery); | ||
logger!.LogInformation("Searching package id for {searchQuery}", SearchQuery); | ||
PackageId = await graphStoreAppUploader!.GetStoreIdForNameAsync(SearchQuery!, cancellationToken); | ||
if (string.IsNullOrEmpty(PackageId)) | ||
{ | ||
logger!.LogError("No package found for {searchQuery}", SearchQuery); | ||
return; | ||
} | ||
} | ||
|
||
// At this moment the package ID should always be filled. | ||
ArgumentException.ThrowIfNullOrWhiteSpace(PackageId); | ||
|
||
logger!.LogInformation("Uploading MSStore app {PackageId} to Intune", PackageId); | ||
var graphServiceClient = CreateGraphServiceClient(httpClient!); | ||
try | ||
{ | ||
var app = await graphStoreAppUploader!.CreateStoreAppAsync(graphServiceClient, PackageId, cancellationToken); | ||
|
||
logger!.LogInformation("Created MSStore app {PackageId} with id {appId}", PackageId, app!.Id); | ||
WriteObject(app); | ||
} | ||
catch (Exception ex) | ||
{ | ||
logger!.LogError(ex, "Error creating MSStore app {PackageId}", PackageId); | ||
} | ||
|
||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Graph.Beta; | ||
using Microsoft.Graph.Beta.Models; | ||
using Microsoft.Graph.Beta.Models.ODataErrors; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using WingetIntune.Models; | ||
|
||
namespace WingetIntune.Graph; | ||
public class GraphStoreAppUploader | ||
{ | ||
private readonly ILogger<GraphStoreAppUploader> logger; | ||
private readonly IFileManager fileManager; | ||
private readonly Internal.MsStore.MicrosoftStoreClient microsoftStoreClient; | ||
private readonly Mapper mapper = new(); | ||
|
||
public GraphStoreAppUploader(ILogger<GraphStoreAppUploader> logger, IFileManager fileManager, Internal.MsStore.MicrosoftStoreClient microsoftStoreClient) | ||
{ | ||
ArgumentNullException.ThrowIfNull(logger); | ||
ArgumentNullException.ThrowIfNull(fileManager); | ||
ArgumentNullException.ThrowIfNull(microsoftStoreClient); | ||
this.logger = logger; | ||
this.fileManager = fileManager; | ||
this.microsoftStoreClient = microsoftStoreClient; | ||
} | ||
|
||
public Task<string?> GetStoreIdForNameAsync(string searchstring, CancellationToken cancellationToken) | ||
{ | ||
ArgumentException.ThrowIfNullOrEmpty(searchstring); | ||
return microsoftStoreClient.GetPackageIdForFirstMatchAsync(searchstring, cancellationToken); | ||
} | ||
|
||
public async Task<WinGetApp?> CreateStoreAppAsync(GraphServiceClient graphServiceClient, string packageId, CancellationToken cancellationToken) | ||
{ | ||
ArgumentNullException.ThrowIfNull(graphServiceClient); | ||
ArgumentException.ThrowIfNullOrEmpty(packageId); | ||
ArgumentNullException.ThrowIfNull(cancellationToken); | ||
|
||
var catalog = await microsoftStoreClient.GetDisplayCatalogAsync(packageId!, cancellationToken); | ||
ArgumentNullException.ThrowIfNull(catalog); | ||
if (!(catalog.Products?.Count() > 0)) | ||
{ | ||
logger.LogError("No products found for {packageId}", packageId); | ||
return null; | ||
} | ||
|
||
var app = mapper.ToWinGetApp(catalog!); | ||
|
||
try | ||
{ | ||
var imagePath = Path.GetTempFileName(); | ||
var uriPart = catalog.Products.First()?.LocalizedProperties.FirstOrDefault()?.Images?.FirstOrDefault(i => i.Height == 300 && i.Width == 300)?.Uri; // && i.ImagePurpose.Equals("Tile", StringComparison.OrdinalIgnoreCase) | ||
if (uriPart is null) | ||
{ | ||
logger.LogWarning("No image found for {packageId}", packageId); | ||
} | ||
else | ||
{ | ||
var imageUrl = $"http:{uriPart}"; | ||
await fileManager.DownloadFileAsync(imageUrl, imagePath, overrideFile: true, cancellationToken: cancellationToken); | ||
app.LargeIcon = new MimeContent | ||
{ | ||
Type = "image/png", | ||
Value = await fileManager.ReadAllBytesAsync(imagePath, cancellationToken) | ||
}; | ||
} | ||
|
||
} | ||
catch (Exception ex) | ||
{ | ||
logger.LogError(ex, "Error downloading image for {packageId}", packageId); | ||
} | ||
|
||
logger.LogInformation("Creating new WinGetApp (MsStore) for {packageId}", packageId); | ||
|
||
try | ||
{ | ||
var createdApp = await graphServiceClient.DeviceAppManagement.MobileApps.PostAsync(app, cancellationToken); | ||
logger.LogInformation("MsStore app {packageIdentifier} created in Intune {appId}", createdApp?.PackageIdentifier, createdApp?.Id); | ||
return createdApp; | ||
} | ||
catch (ODataError ex) | ||
{ | ||
logger.LogError(ex, "Error publishing app {message}", ex.Error?.Message); | ||
throw; | ||
} | ||
catch (Exception ex) | ||
{ | ||
logger.LogError(ex, "Error publishing app"); | ||
throw; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.