Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ bundle
extension-launcher*
internal/customactionplan/testdir/
licenses
main.exe
internal/hostgacommunicator/TestArtifacts/
29 changes: 20 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ BUNDLEDIR=bundle/linux/prod
BUNDLEDIR_TEST=bundle/linux/test
BINDIR=$(BUNDLEDIR)/bin
BINDIR_TEST=$(BUNDLEDIR_TEST)/bin
EXTENSIONVERSION=1.0.18
ALLOWED_EXT1=Microsoft.CPlat.Core.VMApplicationManagerLinux
ALLOWED_EXT2=Microsoft.CPlat.Core.EDP.VMApplicationManagerLinux

Expand All @@ -20,20 +19,20 @@ clean:
-rm -Rf $(BUNDLEDIR_TEST)
-rm -Rf licenses

extension-launcher: validate-extension-name
extension-launcher: validate-extension-name validate-extension-version
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o extension-launcher -ldflags="-X 'main.ExtensionName=$(EXTENSIONNAME)' -X 'main.ExtensionVersion=$(EXTENSIONVERSION)' -X 'main.ExecutableName=vm-application-manager'" ./launcher

extension-launcher-arm64: validate-extension-name
extension-launcher-arm64: validate-extension-name validate-extension-version
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o extension-launcher-arm64 -ldflags="-X 'main.ExtensionName=$(EXTENSIONNAME)' -X 'main.ExtensionVersion=$(EXTENSIONVERSION)' -X 'main.ExecutableName=vm-application-manager'" ./launcher # For ARM64 machines, install command will rename vm-application-manager-arm64 to vm-application-manager

vm-application-manager: validate-extension-name
vm-application-manager: validate-extension-name validate-extension-version
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o vm-application-manager -ldflags="-X 'main.ExtensionName=$(EXTENSIONNAME)' -X 'main.ExtensionVersion=$(EXTENSIONVERSION)'" ./main

vm-application-manager-arm64: validate-extension-name
vm-application-manager-arm64: validate-extension-name validate-extension-version
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o vm-application-manager-arm64 -ldflags="-X 'main.ExtensionName=$(EXTENSIONNAME)' -X 'main.ExtensionVersion=$(EXTENSIONVERSION)'" ./main


.PHONY: validate-extension-name
.PHONY: validate-extension-name validate-extension-version
validate-extension-name:
@case "$(EXTENSIONNAME)" in \
"$(ALLOWED_EXT1)"|"$(ALLOWED_EXT2)" ) ;; \
Expand All @@ -44,15 +43,27 @@ validate-extension-name:
exit 1 ;; \
esac

validate-extension-version:
@if [ -z "$(EXTENSIONVERSION)" ]; then \
echo "Error: EXTENSIONVERSION parameter is required"; \
echo "Usage: make EXTENSIONVERSION=<version>"; \
exit 1; \
fi
@echo "$(EXTENSIONVERSION)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$' || { \
echo "Error: EXTENSIONVERSION '$(EXTENSIONVERSION)' does not match required pattern n.n.n (e.g., 1.0.18)"; \
exit 1; \
}
@echo "Using EXTENSIONVERSION: $(EXTENSIONVERSION)"

collect-licenses:
@echo "Collecting open source licenses..."
@if [ ! -f "$$(go env GOPATH)/bin/go-licenses" ]; then \
echo "Installing go-licenses..."; \
go install github.com/google/go-licenses@latest; \
fi
mkdir -p licenses/reports
$$(go env GOPATH)/bin/go-licenses save ./main --save_path=licenses/texts
$$(go env GOPATH)/bin/go-licenses csv ./main > licenses/reports/THIRD_PARTY_LICENSES.csv
-$$(go env GOPATH)/bin/go-licenses save ./main --save_path=licenses/texts --ignore=std --ignore=golang.org/x/sys
-$$(go env GOPATH)/bin/go-licenses csv ./main --ignore=std --ignore=golang.org/x/sys > licenses/reports/THIRD_PARTY_LICENSES.csv
@echo "License collection complete!"

bundle-prod: extension-launcher extension-launcher-arm64 vm-application-manager vm-application-manager-arm64
Expand All @@ -70,7 +81,7 @@ bundle-prod: extension-launcher extension-launcher-arm64 vm-application-manager

bundle-test:
@echo "Building and packaging TEST bundle into $(BUNDLEDIR_TEST) with EXTENSIONNAME=$(ALLOWED_EXT2)"
$(MAKE) EXTENSIONNAME=$(ALLOWED_EXT2) extension-launcher extension-launcher-arm64 vm-application-manager vm-application-manager-arm64
$(MAKE) EXTENSIONNAME=$(ALLOWED_EXT2) EXTENSIONVERSION=$(EXTENSIONVERSION) extension-launcher extension-launcher-arm64 vm-application-manager vm-application-manager-arm64
mkdir -p $(BINDIR_TEST)
mv extension-launcher "$(BINDIR_TEST)/"
mv extension-launcher-arm64 "$(BINDIR_TEST)/"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Trademarks This project may contain trademarks or logos for projects, products,
- OS-specific tests may be run on an Azure VM or local VM (eg. WSL on Windows)
- To build the extension zip packages
- Windows:
- execute `nmake -f makefile.win`
- execute `nmake -f makefile.win EXTENSIONVERSION=<n.n.n>`
- Linux:
- execute `make`
- execute `make EXTENSIONVERSION=<n.n.n>`

- Please do not check-in vendor files

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.0
toolchain go1.24.3

require (
github.com/Azure/azure-extension-platform v0.0.0-20250107200156-aa20f765d49f
github.com/Azure/azure-extension-platform v0.0.0-20260406194436-44ca1f420dd8
github.com/ahmetalpbalkan/go-httpbin v0.0.0-20200921172446-862fbad56b77
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/Azure/azure-extension-platform v0.0.0-20250107200156-aa20f765d49f h1:ddsUz/suc9txCMz/xWOslqNMvzhbWFMTflUrbcMNoSw=
github.com/Azure/azure-extension-platform v0.0.0-20250107200156-aa20f765d49f/go.mod h1:0458BvQsi5ch6kn+KZtI5m88Z3L9UFXdoY1+6nKdivY=
github.com/Azure/azure-extension-platform v0.0.0-20260406194436-44ca1f420dd8 h1:MwcGMMMhzVioChv9aIe5pbl85WiQuWaR9t+sdZpK3/U=
github.com/Azure/azure-extension-platform v0.0.0-20260406194436-44ca1f420dd8/go.mod h1:0458BvQsi5ch6kn+KZtI5m88Z3L9UFXdoY1+6nKdivY=
github.com/ahmetalpbalkan/go-httpbin v0.0.0-20200921172446-862fbad56b77 h1:QLWeOzO9GTjP14jyM0g7IHhYbnWWR3Wi4kipv3iDOJY=
github.com/ahmetalpbalkan/go-httpbin v0.0.0-20200921172446-862fbad56b77/go.mod h1:Rg55S63lgqSBCawY/oTm7jdFSySp6jwIqgHMB2IeHK8=
github.com/ahmetb/go-httpbin v0.0.0-20200921172446-862fbad56b77 h1:tLnVshegsavDh3VnYwLVgYe7i5/O61LrhKGU+cTR95E=
Expand Down
5 changes: 2 additions & 3 deletions internal/packageregistry/packageregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package packageregistry

import (
"encoding/json"
"io/ioutil"
"os"
"path"
"time"
Expand Down Expand Up @@ -168,7 +167,7 @@ func (self *PackageRegistry) GetExistingPackages() (CurrentPackageRegistry, erro
_, err := os.Stat(localApplicationRegistryFilePath)
if err == nil {
// The file exists
fileBytes, err := ioutil.ReadFile(localApplicationRegistryFilePath)
fileBytes, err := os.ReadFile(localApplicationRegistryFilePath)
if err != nil {
return currentPackageRegistry, err
}
Expand Down Expand Up @@ -210,7 +209,7 @@ func (self *PackageRegistry) WriteToDisk(packageRegistry CurrentPackageRegistry)
return err
}

err = ioutil.WriteFile(regFile, bytes, constants.FilePermissions_UserOnly_ReadWrite)
err = os.WriteFile(regFile, bytes, constants.FilePermissions_UserOnly_ReadWrite)
self.logger.Info("Wrote package registry to %v", regFile)

if doesBackupFileExist {
Expand Down
7 changes: 7 additions & 0 deletions launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ func main() {
eh.Exit(exithelper.MiscError)
}

// validate ExtensionVersion against the version reported by Guest Agent
if extensionVersionFromEnv, err := vmextension.GetGuestAgentEnvironmetVariable(vmextension.GuestAgentEnvVarExtensionVersion); err == nil {
if extensionVersionFromEnv != ExtensionVersion {
el.Warn("ExtensionVersion mismatch: compile-time ExtensionVersion value '%s' does not match value '%s' in environment variable '%s'", ExtensionVersion, extensionVersionFromEnv, vmextension.GuestAgentEnvVarExtensionVersion)
}
}

arg := args[1]

switch arg {
Expand Down
74 changes: 28 additions & 46 deletions main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,16 @@ import (
)

var (
ExtensionName string // assign at compile time
ExtensionVersion = "1.0.10" // should be assigned at compile time, do not edit in code
ExtensionName string // assign at compile time, it is the ExtensionPublisher.ExtensionType
ExtensionVersion = "1.0.10" // should be assigned at compile time, do not edit in code outside of unit tests
reportStatusFunc = utils.ReportStatus
getVMExtensionFunc = getVMExtension
customEnableFunc = customEnable
setSequenceNumberFunc = seqno.SetSequenceNumber
)

const (
vmPackagesSetting = "vmPackages"
operationInstall = "install"
operationUpdate = "update"
operationRemove = "remove"
argVersion = "version"
filelockTimeoutDuration = 45 * time.Minute
)

Expand All @@ -49,19 +46,43 @@ func main() {
}

func getExtensionAndRun(arguments []string) error {
if len(arguments) == 2 && strings.EqualFold(arguments[1], argVersion) {
fmt.Println("Extension version is", ExtensionVersion)
return nil
}

// require SeqNoChange is set to false because we want the extension to ensure that the packages are in sync with the desired packages
ext, err := getVMExtensionFunc()
if err != nil {
return err
}

// validate ExtensionVersion against the version reported by Guest Agent
if extVersionInEnvVariable, err := vmextensionhelper.GetGuestAgentEnvironmetVariable(vmextensionhelper.GuestAgentEnvVarExtensionVersion); err == nil {
if extVersionInEnvVariable != ExtensionVersion {
msg := fmt.Sprintf("ExtensionVersion mismatch: compile-time ExtensionVersion value '%s' does not match value '%s' in environment variable '%s'", ExtensionVersion, extVersionInEnvVariable, vmextensionhelper.GuestAgentEnvVarExtensionVersion)
ext.ExtensionLogger.Warn(msg)
ext.ExtensionEvents.LogWarningEvent("ExtensionVersion", msg)
}
}

if len(arguments) != 2 {
ext.ExtensionLogger.Error("ExtensionError", "vm-application-manager requires an argument")
ext.ExtensionEvents.LogCriticalEvent("ExtensionError", "vm-application-manager requires an argument")
return errors.Errorf("vm-application-manager requires an argument")
}
command := arguments[1]

if command == vmextensionhelper.UpdateOperation.ToString() {
if updateToVersion, err := vmextensionhelper.GetGuestAgentEnvironmetVariable(vmextensionhelper.GuestAgentEnvVarUpdateToVersion); err == nil {
if updateToVersion != ExtensionVersion {
msg := fmt.Sprintf("ExtensionVersion mismatch: compile-time ExtensionVersion value '%s' does not match value '%s' in environment variable '%s'", ExtensionVersion, updateToVersion, vmextensionhelper.GuestAgentEnvVarUpdateToVersion)
ext.ExtensionLogger.Warn(msg)
ext.ExtensionEvents.LogWarningEvent("ExtensionVersion", msg)
}
}
}

pid := os.Getpid()
ext.ExtensionEvents.LogInformationalEvent("vm-application-manager-process", fmt.Sprintf("VmApplications extension starting, PID: %d, Command: %s", pid, command))
defer ext.ExtensionEvents.LogInformationalEvent("vm-application-manager-process", fmt.Sprintf("VmApplications extension exiting, PID: %d, Command: %s", pid, command))
Expand Down Expand Up @@ -124,7 +145,7 @@ func getVMExtension() (*vmextensionhelper.VMExtension, error) {
return nil, err
}

ii.UninstallCallback = vmAppUninstallCallback
ii.UninstallCallback = nil // no need to do any special handling on uninstall, so we can set the callback to nil
ii.UpdateCallback = vmAppUpdateCallback
ii.LogFileNamePattern = "VmAppExt_%v.log"

Expand Down Expand Up @@ -257,42 +278,3 @@ func customEnable(ext *vmextensionhelper.VMExtension, hostgaCommunicator hostgac

return nil
}

// Callback indicating the extension is being removed
func vmAppUninstallCallback(ext *vmextensionhelper.VMExtension) error {
ext.ExtensionEvents.LogInformationalEvent("Uninstalling", "VmApplications extension - removing all applications for uninstall")
hostGaCommunicator := hostgacommunicator.HostGaCommunicator{}
err := doVmAppUninstallCallback(ext, &hostGaCommunicator)
if err == nil {
ext.ExtensionEvents.LogInformationalEvent("Completed", "VmApplications extension uninstalled. Result=Success")
} else {
ext.ExtensionEvents.LogInformationalEvent(
"Completed",
fmt.Sprintf("VmApplications extension uninstall finished. Result=Failure;Reason=%v", err.Error()))
}
return err
}

func doVmAppUninstallCallback(ext *vmextensionhelper.VMExtension, hostGaCommunicator hostgacommunicator.IHostGaCommunicator) error {
packageRegistry, err := packageregistry.New(ext.ExtensionLogger, ext.HandlerEnv, filelockTimeoutDuration)
if err != nil {
return errors.Wrapf(err, "Could not create package registry")
}
defer packageRegistry.Close()

currentPackageRegistry, err := packageRegistry.GetExistingPackages()
if err != nil {
return errors.Wrapf(err, "Could not read current package registry")
}

// Create an empty incoming collection so we'll create an action plan to remove all applications
emptyIncomingCollection := make(packageregistry.VMAppPackageIncomingCollection, 0)

actionPlan := actionplan.New(currentPackageRegistry, emptyIncomingCollection, ext.HandlerEnv, hostGaCommunicator, ext.ExtensionLogger)
commandHandler := commandhandler.CommandHandler{}

// Removing applications is best effort, so even if there are errors here, we ignore them
_, _ = actionPlan.Execute(packageRegistry, ext.ExtensionEvents, &commandHandler)

return nil
}
79 changes: 0 additions & 79 deletions main/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
handlersettings "github.com/Azure/azure-extension-platform/pkg/settings"
"github.com/Azure/azure-extension-platform/pkg/status"
"github.com/Azure/azure-extension-platform/vmextension"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -466,84 +465,6 @@ func Test_main_nothingToProcess_withStatus(t *testing.T) {
require.Equal(t, requestedSequenceNumber, currentSequenceNumber)
}

func Test_uninstall_cannotCreatePackageRegistry(t *testing.T) {
vmApplications := []extdeserialization.VmAppSetting{}
ext := createTestVMExtension(t, vmApplications)
hostGaCommunicator := NoopHostGaCommunicator{}

// Set the config folder to an invalid path so we can't create a package registry
ext.HandlerEnv.ConfigFolder = "/yabaflarg/flarpaglarp"

err := doVmAppUninstallCallback(ext, &hostGaCommunicator)
require.Error(t, err)
require.EqualError(t, err, cannotCreatePackageRegistryError)
}

func Test_uninstall_cannotReadPackageRegistry(t *testing.T) {
vmApplications := []extdeserialization.VmAppSetting{}
ext := createTestVMExtension(t, vmApplications)
hostGaCommunicator := NoopHostGaCommunicator{}

// Write an invalid registry so we can't create a package registry
appRegistryFilePath := path.Join(ext.HandlerEnv.ConfigFolder, packageregistry.LocalApplicationRegistryFileName)
ioutil.WriteFile(appRegistryFilePath, []byte("}"), 0644)
defer os.Remove(appRegistryFilePath)

err := doVmAppUninstallCallback(ext, &hostGaCommunicator)
require.Error(t, err)
require.EqualError(t, err, "Could not read current package registry: invalid character '}' looking for beginning of value")
}

func Test_uninstall_noAppsToUninstall(t *testing.T) {
vmApplications := []extdeserialization.VmAppSetting{}
ext := createTestVMExtension(t, vmApplications)
hostGaCommunicator := NoopHostGaCommunicator{}

package1 := path.Join(ext.HandlerEnv.ConfigFolder, "package1")
package2 := path.Join(ext.HandlerEnv.ConfigFolder, "package2")
package1Quotes := fmt.Sprintf("\"%v\"", package1)
package2Quotes := fmt.Sprintf("\"%v\"", package2)

// Create a package registry where the remove commands will write their respective files
reg := packageregistry.CurrentPackageRegistry{"package1": &packageregistry.VMAppPackageCurrent{
ApplicationName: "package1",
DirectDownloadOnly: false,
InstallCommand: "dontcare",
RemoveCommand: "echo moein > " + package1Quotes,
UpdateCommand: "dontcare",
Version: "1.2.3.1",
}, "package2": &packageregistry.VMAppPackageCurrent{
ApplicationName: "package2",
DirectDownloadOnly: true,
InstallCommand: "dontcare",
RemoveCommand: "echo moein > " + package2Quotes,
UpdateCommand: "dontcare",
Version: "1.2.3.2",
}}

pkgHndlr, err := packageregistry.New(nopLog(), ext.HandlerEnv, time.Second)
assert.NoError(t, err, "operation should not throw error")
err = pkgHndlr.WriteToDisk(reg)
assert.NoError(t, err, "Should be able to write package registry to disk")
pkgHndlr.Close()

err = doVmAppUninstallCallback(ext, &hostGaCommunicator)
require.NoError(t, err)

// Verify we removed both apps, which deleted the files
require.True(t, fileExists(package1), "First application was not removed")
require.True(t, fileExists(package2), "Second application was not removed")
}

func Test_uninstall_uninstallApps(t *testing.T) {
vmApplications := []extdeserialization.VmAppSetting{}
ext := createTestVMExtension(t, vmApplications)
hostGaCommunicator := NoopHostGaCommunicator{}

err := doVmAppUninstallCallback(ext, &hostGaCommunicator)
require.NoError(t, err)
}

func fileExists(filePath string) bool {
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
return false
Expand Down
Loading
Loading