Skip to content

Commit 1a5d544

Browse files
George Milekagmileka
andauthored
feat: initial azldev support for image customization (#320)
* Add the image command * Builds with image customizer * Ensure env.Context() return the internal ctx object. * Remove the requirement that azldev is run as a non-root. * We can use azldev to customize an image now * Add all functional commands and sub-commands of the imagecustomizer into azldev. * Block dry-run modes for image commands * Add log-level to azldev * Add --containerized switch * Enable running in a container * Fix container build, remove ic golang pkg calls, and extend azldev log level. * Move the container tag to the project settings * Re-enable the check for running as root * Remove the --containerized switch * Move runDocker to imageutils.go * Switch inject-files to using the containerized version * Stop using local azure-linux-image-tools * Consolidate code in the imageutils.go file. * Fix rpm-source parameter * Add support for optional cli switches * Update project schema with new types * Existing unit tests now pass * Add unit tests for image commands * Remove new log level * auto-generate the schema * Update scenario tests * Fix all lint errors * Fix build errors post rebase on main * Rename ImageCustomizerConfig to ImageCustomizer * Add comment on why we are requiring image customizer optional parameters * Consolidate the code to build the supported image formats * Remove dependency on Image Customization library. * Fix comment typos * Rename --output-image-name to --output-path * Fix typo in getImageCustomizerImageFormats name * Mark read-only mounted folders as read-only * Add custom auto-completion to the --output-image-format switch * Remove the hard-coded image customizer container tag from the code * Fix the scenarios break * Move RunDocker() to a separate utils file * Refactor runImageCustomizerContainer() into smaller functions * Add missing docker.go --------- Co-authored-by: George Mileka <gmileka@outlook.com>
1 parent cb60150 commit 1a5d544

29 files changed

Lines changed: 626 additions & 22 deletions

cmd/azldev/azldev.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/microsoft/azldev/internal/app/azldev/cmds/component"
1313
"github.com/microsoft/azldev/internal/app/azldev/cmds/config"
1414
"github.com/microsoft/azldev/internal/app/azldev/cmds/docs"
15+
"github.com/microsoft/azldev/internal/app/azldev/cmds/image"
1516
"github.com/microsoft/azldev/internal/app/azldev/cmds/project"
1617
"github.com/microsoft/azldev/internal/app/azldev/cmds/version"
1718
)
@@ -44,6 +45,7 @@ func InstantiateApp() *azldev.App {
4445
component.OnAppInit(app)
4546
config.OnAppInit(app)
4647
docs.OnAppInit(app)
48+
image.OnAppInit(app)
4749
project.OnAppInit(app)
4850
version.OnAppInit(app)
4951

cmd/azldev/azldev_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestInstantiateApp(t *testing.T) {
2626
"component",
2727
"config",
2828
"docs",
29+
"image",
2930
"project",
3031
"version",
3132
},

defaultconfigs/content/defaults.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ includes = ["distros/**/*.distro.toml"]
33

44
[project]
55
default-distro = { name = "azurelinux", version = "3.0" }
6+
7+
[tools.imageCustomizer]
8+
containerTag = "mcr.microsoft.com/azurelinux/imagecustomizer:0.18.0"

go.mod

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/microsoft/azldev
22

3-
go 1.24.0
3+
go 1.24.1
44

55
toolchain go1.24.3
66

@@ -138,17 +138,14 @@ require (
138138
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
139139
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
140140
go.opentelemetry.io/otel v1.38.0 // indirect
141-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
141+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
142142
go.opentelemetry.io/otel/metric v1.38.0 // indirect
143143
go.opentelemetry.io/otel/trace v1.38.0 // indirect
144-
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
144+
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
145145
golang.org/x/crypto v0.42.0 // indirect
146146
golang.org/x/term v0.35.0 // indirect
147147
golang.org/x/text v0.29.0 // indirect
148-
google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e // indirect
149-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
150-
google.golang.org/grpc v1.71.0 // indirect
151-
google.golang.org/protobuf v1.36.5 // indirect
148+
google.golang.org/protobuf v1.36.8 // indirect
152149
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
153150
gopkg.in/yaml.v3 v3.0.1 // indirect
154151
)

go.sum

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU
133133
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
134134
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
135135
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
136-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
137-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
136+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
137+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
138138
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
139139
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
140140
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@@ -299,8 +299,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
299299
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
300300
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
301301
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
302-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
303-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
302+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
303+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
304304
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
305305
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
306306
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
@@ -311,8 +311,8 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6
311311
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
312312
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
313313
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
314-
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
315-
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
314+
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
315+
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
316316
go.szostok.io/version v1.2.0 h1:8eMMdfsonjbibwZRLJ8TnrErY8bThFTQsZYV16mcXms=
317317
go.szostok.io/version v1.2.0/go.mod h1:EiU0gPxaXb6MZ+apSN0WgDO6F4JXyC99k9PIXf2k2E8=
318318
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -342,14 +342,14 @@ golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
342342
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
343343
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
344344
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
345-
google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e h1:nsxey/MfoGzYNduN0NN/+hqP9iiCIYsrVbXb/8hjFM8=
346-
google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA=
347-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
348-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
349-
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
350-
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
351-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
352-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
345+
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
346+
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
347+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
348+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
349+
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
350+
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
351+
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
352+
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
353353
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
354354
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
355355
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

internal/app/azldev/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ func (a *App) Execute(args []string) int {
284284
env.SetVerbose(a.verbose)
285285
env.SetQuiet(a.quiet)
286286

287+
env.SetColorMode(a.colorMode)
287288
//
288289
// If we managed to find a project + configuration, then we can let anyone who was
289290
// interested have an opportunity to add subcommands (or do whatever they need to
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package image
5+
6+
import (
7+
"github.com/microsoft/azldev/internal/app/azldev"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// Options for adding components to the project configuration.
12+
func customizeOnAppInit(_ *azldev.App, parentCmd *cobra.Command) {
13+
parentCmd.AddCommand(NewImageCustomizeCmd())
14+
}
15+
16+
// Constructs a [cobra.Command] for the 'project new' command.
17+
func NewImageCustomizeCmd() *cobra.Command {
18+
options := &imageCustomizerOptions{}
19+
20+
// We don't *require* a valid project configuration, but may use one if it's available.
21+
cmd := &cobra.Command{
22+
Use: "customize",
23+
Short: "Customizes a pre-built Azure Linux image",
24+
RunE: azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) {
25+
return true, customizeImage(env, options)
26+
}),
27+
}
28+
29+
cmd.Flags().StringVarP(&options.imageFile, "image-file", "", "",
30+
"Path of the base Azure Linux image which the customization will be applied to")
31+
cmd.Flags().StringVarP(&options.configFile, "config-file", "", "",
32+
"Path of the image customization config file")
33+
cmd.Flags().StringVar(&options.outputImageFormat, "output-image-format", "",
34+
"Format of output image ("+getImageCustomizerImageFormatsString()+")")
35+
cmd.Flags().StringVar(&options.outputPath, "output-path", "",
36+
"Path to write the customized image artifacts to. It can be a path to "+
37+
"a file if the output format is a single file format (e.g., vhd, qcow2), "+
38+
"or a path to a directory if the output format is a multi-file format (e.g., pxe-dir, pxe-tar)")
39+
cmd.Flags().StringSliceVar(&options.rpmSources, "rpm-source", []string{},
40+
"Path to a RPM repo config file or a directory containing RPMs")
41+
cmd.Flags().BoolVar(&options.disableBaseImageRpmRepos, "disable-base-image-rpm-repos", false,
42+
"Disable the base image's RPM repos as an RPM source")
43+
cmd.Flags().StringVar(&options.packageSnapshotTime, "package-snapshot-time", "",
44+
"Only packages published before this snapshot time will be available during customization."+
45+
" Supports 'YYYY-MM-DD' or full RFC3339 timestamp (e.g., 2024-05-20T23:59:59Z)")
46+
47+
_ = cmd.RegisterFlagCompletionFunc("output-image-format",
48+
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
49+
return getImageCustomizerImageFormats(), cobra.ShellCompDirectiveDefault
50+
})
51+
52+
// While --image-file and --output-path are not required by the Image
53+
// Customizer, we are requiring them in azldev so that we can deduce which
54+
// folders to mount into the container.
55+
// See: https://dev.azure.com/mariner-org/polar/_workitems/edit/15282
56+
_ = cmd.MarkFlagRequired("image-file")
57+
_ = cmd.MarkFlagRequired("config-file")
58+
_ = cmd.MarkFlagRequired("output-path")
59+
60+
return cmd
61+
}
62+
63+
func customizeImage(
64+
env *azldev.Env, options *imageCustomizerOptions,
65+
) error {
66+
return runImageCustomizerContainer(env, imageCustomizerCustomize, options)
67+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package image_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/microsoft/azldev/internal/app/azldev/cmds/image"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestNewImageCustomizeCmd(t *testing.T) {
15+
cmd := image.NewImageCustomizeCmd()
16+
require.NotNil(t, cmd)
17+
assert.Equal(t, "customize", cmd.Use)
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package image
5+
6+
import (
7+
"github.com/microsoft/azldev/internal/app/azldev"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// Called once when the app is initialized; registers any commands or callbacks with the app.
12+
func OnAppInit(app *azldev.App) {
13+
cmd := &cobra.Command{
14+
Use: "image",
15+
Short: "Manage Azure Linux images",
16+
}
17+
18+
app.AddTopLevelCommand(cmd)
19+
customizeOnAppInit(app, cmd)
20+
injectFilesOnAppInit(app, cmd)
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package image_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/microsoft/azldev/internal/app/azldev"
10+
"github.com/microsoft/azldev/internal/app/azldev/cmds/image"
11+
"github.com/microsoft/azldev/internal/global/opctx/opctx_test"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"go.uber.org/mock/gomock"
15+
)
16+
17+
func TestOnAppInit(t *testing.T) {
18+
ctrl := gomock.NewController(t)
19+
app := azldev.NewApp(opctx_test.NewMockFileSystemFactory(ctrl), opctx_test.NewMockOSEnvFactory(ctrl))
20+
21+
image.OnAppInit(app)
22+
23+
// Make sure the docs command was added to the app.
24+
topLevelCommandNames, err := app.CommandNames()
25+
require.NoError(t, err)
26+
27+
assert.Contains(t, topLevelCommandNames, "image")
28+
}

0 commit comments

Comments
 (0)