Skip to content

fix: updated win_get examples #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,63 @@
resource "microsoft365_graph_beta_device_and_app_management_win_get_app" "whatsapp" {
package_identifier = "9NKSQGP7F2NH" # The unique identifier for the app obtained from msft app store
automatically_generate_metadata = true

# Install experience settings
install_experience = {
run_as_account = "user" # Can be 'system' or 'user'
}

role_scope_tag_ids = ["0"]

# Optional fields
is_featured = true
privacy_information_url = "https://privacy.example.com"
information_url = "https://info.example.com"
owner = "example-owner"
developer = "example-developer"
notes = "Some relevant notes for this app."

# Optional: Define custom timeouts
timeouts = {
create = "10m"
update = "10m"
delete = "5m"
}
}

resource "microsoft365_graph_beta_device_and_app_management_win_get_app" "Adobe_Acrobat_Reader_DC" {
package_identifier = "xpdp273c0xhqh2" # The unique identifier for the app obtained from msft app store
automatically_generate_metadata = false
display_name = "Adobe Acrobat Reader DC"
description = "Adobe Acrobat Reader DC is the free, trusted standard for viewing, printing, signing, and annotating PDFs. It's the only PDF viewer that can open and interact with all types of PDF content – including forms and multimedia."
publisher = "Adobe Inc."
large_icon = {
type = "image/png"
value = filebase64("${path.module}/Adobe_Reader_XI_icon.png")
}
# Install experience settings
install_experience = {
run_as_account = "user" # Can be 'system' or 'user'
}

role_scope_tag_ids = ["0"]

# Optional fields
is_featured = true
privacy_information_url = "https://privacy.example.com"
information_url = "https://info.example.com"
owner = "example-owner"
developer = "example-developer"
notes = "Some relevant notes for this app."

# Optional: Define custom timeouts
timeouts = {
create = "10m"
update = "10m"
delete = "5m"
}
}

resource "microsoft365_graph_beta_device_and_app_management_win_get_app" "visual_studio_code" {
package_identifier = "XP9KHM4BK9FZ7Q" # The unique identifier for the app obtained from msft app store

Expand Down
37 changes: 37 additions & 0 deletions internal/resources/common/plan_modifiers/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package planmodifiers
import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
Expand Down Expand Up @@ -119,3 +120,39 @@ func DefaultValueString(defaultValue string) StringModifier {
defaultValue: types.StringValue(defaultValue),
}
}

// caseInsensitiveString handles case-insensitive string comparisons
type caseInsensitiveString struct {
stringModifier
}

func (m caseInsensitiveString) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
// For config values that don't match state, preserve their case
if req.ConfigValue.IsNull() || req.StateValue.IsNull() {
resp.PlanValue = req.ConfigValue
return
}

if strings.EqualFold(req.PlanValue.ValueString(), req.StateValue.ValueString()) {
resp.PlanValue = req.StateValue
return
}

// Allow either case from config
if strings.EqualFold(req.PlanValue.ValueString(), req.ConfigValue.ValueString()) {
resp.PlanValue = req.ConfigValue
return
}

resp.PlanValue = types.StringValue(strings.ToUpper(req.PlanValue.ValueString()))
}

// CaseInsensitiveString returns a plan modifier for case-insensitive string handling
func CaseInsensitiveString() StringModifier {
return caseInsensitiveString{
stringModifier: stringModifier{
description: "Handles case-insensitive string comparisons",
markdownDescription: "Handles case-insensitive string comparisons",
},
}
}
5 changes: 0 additions & 5 deletions internal/resources/common/utilities/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ func StringPtr(s string) *string {
return &s
}

// Helper function to convert a string to uppercase
func ToUpperCase(s string) string {
return strings.ToUpper(s)
}

// DownloadImage downloads an image from a given URL and returns it as a byte slice
// with retries
func DownloadImage(url string) ([]byte, error) {
Expand Down
55 changes: 0 additions & 55 deletions internal/resources/common/utilities/utilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,61 +223,6 @@ func TestStringPtr(t *testing.T) {
})
}

// TestToUpperCase tests the ToUpperCase function.
func TestToUpperCase(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "Lowercase string",
input: "hello",
want: "HELLO",
},
{
name: "Mixed case string",
input: "HeLLo WoRLd",
want: "HELLO WORLD",
},
{
name: "Already uppercase",
input: "GOLANG",
want: "GOLANG",
},
{
name: "Empty string",
input: "",
want: "",
},
{
name: "Numeric string",
input: "123abc",
want: "123ABC",
},
{
name: "Unicode characters",
input: "こんにちは",
want: "こんにちは", // Uppercasing has no effect on non-Latin scripts
},
{
name: "Special characters",
input: "go-lang_2024!",
want: "GO-LANG_2024!",
},
}

for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
got := ToUpperCase(tt.input)
if got != tt.want {
t.Errorf("ToUpperCase() = %v, want %v", got, tt.want)
}
})
}
}

// TestDownloadImage tests the DownloadImage function.
func TestDownloadImage(t *testing.T) {
// Define a sample image data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package graphBetaWinGetApp

import (
"context"
"encoding/base64"
"fmt"
"strings"

"github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/construct"
utils "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common/utilities"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
graphmodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models"
)
Expand All @@ -18,41 +23,59 @@ func constructResource(ctx context.Context, data *WinGetAppResourceModel) (graph
requestBody := graphmodels.NewWinGetApp()

packageIdentifier := data.PackageIdentifier.ValueString()
upperPackageIdentifier := utils.ToUpperCase(packageIdentifier)
upperPackageIdentifier := strings.ToUpper(packageIdentifier)
requestBody.SetPackageIdentifier(&upperPackageIdentifier)

// Fetch metadata from the Microsoft Store using the packageIdentifier
title, imageURL, description, publisher, err := FetchStoreAppDetails(ctx, packageIdentifier)
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to fetch store details for packageIdentifier '%s': %v", packageIdentifier, err))
return nil, fmt.Errorf("failed to fetch store details for packageIdentifier '%s': %v", packageIdentifier, err)
}
// Fetch metadata from the Microsoft Store using the packageIdentifier if AutomaticallyGenerateMetadata is true
if data.AutomaticallyGenerateMetadata.ValueBool() {
title, imageURL, description, publisher, err := FetchStoreAppDetails(ctx, packageIdentifier)
if err != nil {
return nil, fmt.Errorf("failed to fetch store details for packageIdentifier '%s': %v", packageIdentifier, err)
}

// Check if any required value (title, description, publisher) is empty
if title == "" || description == "" || publisher == "" {
errMsg := fmt.Sprintf("Incomplete store details for packageIdentifier '%s'. Missing required fields: Title='%s', Description='%s', Publisher='%s'", packageIdentifier, title, description, publisher)
tflog.Error(ctx, errMsg)
return nil, fmt.Errorf("incomplete store details for packageIdentifier '%s'. Missing required fields: Title='%s', Description='%s', Publisher='%s'", packageIdentifier, title, description, publisher)
}
if title == "" || description == "" || publisher == "" {
return nil, fmt.Errorf("incomplete store details for packageIdentifier '%s'. Missing required fields: Title='%s', Description='%s', Publisher='%s'", packageIdentifier, title, description, publisher)
}

requestBody.SetDisplayName(&title)
requestBody.SetDescription(&description)
requestBody.SetPublisher(&publisher)
requestBody.SetDisplayName(&title)
requestBody.SetDescription(&description)
requestBody.SetPublisher(&publisher)

if imageURL != "" {
iconBytes, err := utils.DownloadImage(imageURL)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("Failed to download icon image from URL '%s': %v", imageURL, err))
} else {
largeIcon := graphmodels.NewMimeContent()
iconType := "image/png"
largeIcon.SetTypeEscaped(&iconType)
largeIcon.SetValue(iconBytes)
requestBody.SetLargeIcon(largeIcon)
}
}
} else {
// Use the provided values from the model
construct.SetStringProperty(data.Description, requestBody.SetDescription)
construct.SetStringProperty(data.Publisher, requestBody.SetPublisher)
construct.SetStringProperty(data.DisplayName, requestBody.SetDisplayName)

if imageURL != "" {
iconBytes, err := utils.DownloadImage(imageURL)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("Failed to download icon image from URL '%s': %v. Continuing without setting the icon.", imageURL, err))
} else {
if !data.LargeIcon.IsNull() {
largeIcon := graphmodels.NewMimeContent()
var iconData map[string]attr.Value
data.LargeIcon.As(context.Background(), &iconData, basetypes.ObjectAsOptions{})

iconType := "image/png"
largeIcon.SetTypeEscaped(&iconType)
largeIcon.SetValue(iconBytes)

if valueVal, ok := iconData["value"].(types.String); ok {
iconBytes, err := base64.StdEncoding.DecodeString(valueVal.ValueString())
if err != nil {
return nil, fmt.Errorf("failed to decode icon base64: %v", err)
}
largeIcon.SetValue(iconBytes)
}
requestBody.SetLargeIcon(largeIcon)
tflog.Debug(ctx, fmt.Sprintf("Icon set from store URL. Data length: %d bytes", len(iconBytes)))
}
} else {
tflog.Debug(ctx, fmt.Sprintf("No icon image URL found for packageIdentifier '%s'. The large icon will not be set.", packageIdentifier))
}

construct.SetBoolProperty(data.IsFeatured, requestBody.SetIsFeatured)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// REF: https://learn.microsoft.com/en-us/graph/api/resources/intune-shared-mobileapp?view=graph-rest-beta
// REF: https://learn.microsoft.com/en-us/graph/api/resources/intune-apps-wingetapp?view=graph-rest-beta
package graphBetaWinGetApp

Expand All @@ -9,31 +10,32 @@ import (

// WinGetAppResourceModel represents the Terraform resource model for a WinGetApp
type WinGetAppResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
Publisher types.String `tfsdk:"publisher"`
LargeIcon types.Object `tfsdk:"large_icon"`
CreatedDateTime types.String `tfsdk:"created_date_time"`
LastModifiedDateTime types.String `tfsdk:"last_modified_date_time"`
IsFeatured types.Bool `tfsdk:"is_featured"`
PrivacyInformationUrl types.String `tfsdk:"privacy_information_url"`
InformationUrl types.String `tfsdk:"information_url"`
Owner types.String `tfsdk:"owner"`
Developer types.String `tfsdk:"developer"`
Notes types.String `tfsdk:"notes"`
UploadState types.Int64 `tfsdk:"upload_state"`
PublishingState types.String `tfsdk:"publishing_state"`
IsAssigned types.Bool `tfsdk:"is_assigned"`
RoleScopeTagIds types.List `tfsdk:"role_scope_tag_ids"`
DependentAppCount types.Int64 `tfsdk:"dependent_app_count"`
SupersedingAppCount types.Int64 `tfsdk:"superseding_app_count"`
SupersededAppCount types.Int64 `tfsdk:"superseded_app_count"`
ManifestHash types.String `tfsdk:"manifest_hash"`
PackageIdentifier types.String `tfsdk:"package_identifier"`
InstallExperience *WinGetAppInstallExperienceResourceModel `tfsdk:"install_experience"`
Assignments *sharedmodels.MobileAppAssignmentResourceModel `tfsdk:"assignments"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
Publisher types.String `tfsdk:"publisher"`
LargeIcon types.Object `tfsdk:"large_icon"`
CreatedDateTime types.String `tfsdk:"created_date_time"`
LastModifiedDateTime types.String `tfsdk:"last_modified_date_time"`
IsFeatured types.Bool `tfsdk:"is_featured"`
PrivacyInformationUrl types.String `tfsdk:"privacy_information_url"`
InformationUrl types.String `tfsdk:"information_url"`
Owner types.String `tfsdk:"owner"`
Developer types.String `tfsdk:"developer"`
Notes types.String `tfsdk:"notes"`
UploadState types.Int64 `tfsdk:"upload_state"`
PublishingState types.String `tfsdk:"publishing_state"`
IsAssigned types.Bool `tfsdk:"is_assigned"`
RoleScopeTagIds types.List `tfsdk:"role_scope_tag_ids"`
DependentAppCount types.Int64 `tfsdk:"dependent_app_count"`
SupersedingAppCount types.Int64 `tfsdk:"superseding_app_count"`
SupersededAppCount types.Int64 `tfsdk:"superseded_app_count"`
ManifestHash types.String `tfsdk:"manifest_hash"`
PackageIdentifier types.String `tfsdk:"package_identifier"`
AutomaticallyGenerateMetadata types.Bool `tfsdk:"automatically_generate_metadata"`
InstallExperience *WinGetAppInstallExperienceResourceModel `tfsdk:"install_experience"`
Assignments *sharedmodels.MobileAppAssignmentResourceModel `tfsdk:"assignments"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

// WinGetAppInstallExperienceModel represents the install experience structure
Expand Down
Loading
Loading