Skip to content
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
53 changes: 43 additions & 10 deletions cmd/alzlibtool/command/cache/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ When --library and --architecture are specified, only the definitions referenced
architecture are included, producing a smaller, use-case-specific cache. This is useful for
embedding the minimal set of definitions needed for a given deployment workflow.

A --library value may be either a local path (e.g. ./mylib) or an ALZ Library reference in
the form <member>@<ref>, e.g. platform/alz@2026.01.3. The flag may be specified multiple
times to combine several libraries; their dependencies are fetched recursively.

Use --library-overwrite-enabled to allow later libraries to overwrite definitions, policy
assignments, role definitions, archetypes and architectures already provided by earlier
libraries. This is useful when layering a custom library on top of a base ALZ library.

Use --from-cache to seed from an existing cache file (requires --library and --architecture).
Definitions already present in the seed cache are used directly and not re-fetched from Azure,
reducing the number of API calls. The same file may be used for both --from-cache and --output
Expand All @@ -44,12 +52,13 @@ to update a cache in-place.`,
Run: func(cmd *cobra.Command, _ []string) {
outFile, _ := cmd.Flags().GetString("output")
verbose, _ := cmd.Flags().GetBool("verbose")
libraryPath, _ := cmd.Flags().GetString("library")
libraryRefs, _ := cmd.Flags().GetStringArray("library")
architectureName, _ := cmd.Flags().GetString("architecture")
fromCacheFile, _ := cmd.Flags().GetString("from-cache")
libraryOverwriteEnabled, _ := cmd.Flags().GetBool("library-overwrite-enabled")

// --library and --architecture must be specified together.
if (libraryPath == "") != (architectureName == "") {
if (len(libraryRefs) == 0) != (architectureName == "") {
cmd.PrintErrf(
"%s --library and --architecture must be specified together\n",
cmd.ErrPrefix(),
Expand All @@ -61,14 +70,24 @@ to update a cache in-place.`,
// lazy cache-first lookup can skip individual Azure API calls. In full-scan
// mode the bulk listing APIs fetch everything regardless, so a seed cache
// cannot reduce work.
if fromCacheFile != "" && libraryPath == "" {
if fromCacheFile != "" && len(libraryRefs) == 0 {
cmd.PrintErrf(
"%s --from-cache requires --library and --architecture\n",
cmd.ErrPrefix(),
)
os.Exit(1)
}

// --library-overwrite-enabled is only meaningful in architecture-scoped mode
// where multiple libraries are loaded into an AlzLib instance.
if libraryOverwriteEnabled && len(libraryRefs) == 0 {
cmd.PrintErrf(
"%s --library-overwrite-enabled requires --library and --architecture\n",
cmd.ErrPrefix(),
)
os.Exit(1)
}

// Read the seed cache BEFORE opening the output file, because --from-cache
// and --output may point to the same path.
var seedCache *cache.Cache
Expand Down Expand Up @@ -106,12 +125,15 @@ to update a cache in-place.`,

var resultCache *cache.Cache

if libraryPath != "" {
if len(libraryRefs) > 0 {
// Architecture-scoped mode: process the library + architecture and export
// only the built-in definitions that were actually referenced.
thisLib := alzlib.NewCustomLibraryReference(libraryPath)
refs := make(alzlib.LibraryReferences, 0, len(libraryRefs))
for _, r := range libraryRefs {
refs = append(refs, alzlib.NewLibraryReference(r))
}

allLibs, err := thisLib.FetchWithDependencies(cmd.Context())
allLibs, err := refs.FetchWithDependencies(cmd.Context())
if err != nil {
cmd.PrintErrf(
"%s could not fetch libraries with dependencies: %v\n",
Expand All @@ -121,6 +143,7 @@ to update a cache in-place.`,
}

az := alzlib.NewAlzLib(nil)
az.Options.AllowOverwrite = libraryOverwriteEnabled

if seedCache != nil {
az.AddCache(seedCache)
Expand Down Expand Up @@ -221,10 +244,13 @@ func init() {
createCmd.Flags().
BoolP("verbose", "v", false, "Display detailed progress during cache creation.")
createCmd.Flags().
StringP(
"library", "L", "",
"Path to a library. When set together with --architecture, creates a minimal cache "+
"containing only the definitions referenced by the specified architecture.")
StringArrayP(
"library", "L", nil,
"Path or reference to a library. May be specified multiple times. Each value is "+
"either a local filesystem path (e.g. ./mylib) or an ALZ Library reference of "+
"the form <member>@<ref> (e.g. platform/alz@2026.01.3). When set together with "+
"--architecture, creates a minimal cache containing only the definitions "+
"referenced by the specified architecture.")
createCmd.Flags().
StringP(
"architecture", "a", "",
Expand All @@ -235,4 +261,11 @@ func init() {
"Path to an existing cache file to use as a seed (requires --library and --architecture). "+
"Definitions found in the seed cache are not re-fetched from Azure, reducing API calls. "+
"The same path may be used for both --from-cache and --output to update a cache in-place.")
createCmd.Flags().
Bool(
"library-overwrite-enabled", false,
"Allow later libraries to overwrite definitions, policy assignments, role definitions, "+
"archetypes and architectures already provided by earlier libraries. Useful when "+
"layering a custom library on top of a base ALZ library. Requires --library and "+
"--architecture.")
}
65 changes: 65 additions & 0 deletions metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ func NewAlzLibraryReference(path, ref string) *AlzLibraryReference {
}
}

// NewAlzLibraryReferenceFromString creates a new AlzLibraryReference by parsing
// a string in the form "<path>@<ref>" (the inverse of AlzLibraryReference.String()).
// Returns an error if s does not contain an "@" separator, has an empty path or
// has an empty ref.
func NewAlzLibraryReferenceFromString(s string) (*AlzLibraryReference, error) {
idx := strings.LastIndex(s, "@")
if idx <= 0 {
return nil, fmt.Errorf(
"NewAlzLibraryReferenceFromString: %q is not in the form <path>@<ref>", s,
)
}

path := s[:idx]
ref := s[idx+1:]

if ref == "" {
return nil, fmt.Errorf(
"NewAlzLibraryReferenceFromString: %q has an empty ref", s,
)
}

return NewAlzLibraryReference(path, ref), nil
}

// NewAlzLibraryReferenceFromFS creates a new AlzLibraryReference with the given path, ref and filesystem.
func NewAlzLibraryReferenceFromFS(path, ref string, filesystem fs.FS) *AlzLibraryReference {
return &AlzLibraryReference{
Expand Down Expand Up @@ -173,6 +197,14 @@ func NewCustomLibraryReference(url string) *CustomLibraryReference {
}
}

// NewCustomLibraryReferenceFromString creates a new CustomLibraryReference from
// a go-getter URL or local path string (the inverse of CustomLibraryReference.String()).
// It is equivalent to NewCustomLibraryReference and is provided for symmetry with
// NewAlzLibraryReferenceFromString.
func NewCustomLibraryReferenceFromString(s string) *CustomLibraryReference {
return NewCustomLibraryReference(s)
}

// NewCustomLibraryReferenceFromFS creates a new CustomLibraryReference with the given URL and filesystem.
func NewCustomLibraryReferenceFromFS(url string, filesystem fs.FS) *CustomLibraryReference {
return &CustomLibraryReference{
Expand Down Expand Up @@ -288,3 +320,36 @@ func (m *Metadata) IsAlzLibraryRef() bool {
func (m *Metadata) Ref() LibraryReference {
return m.ref
}

// NewLibraryReference is a universal constructor that creates a LibraryReference
// from a string by detecting its form. Values that contain an "@" separator and
// do not look like a local filesystem path (i.e. do not start with ".", "/", "\"
// or a Windows drive letter such as "C:") are treated as ALZ Library references
// of the form "<path>@<ref>" and an *AlzLibraryReference is returned. Everything
// else is returned as a *CustomLibraryReference.
func NewLibraryReference(s string) LibraryReference {
if !looksLikeLocalPath(s) {
if ref, err := NewAlzLibraryReferenceFromString(s); err == nil {
return ref
}
}

return NewCustomLibraryReference(s)
}

// looksLikeLocalPath reports whether p has a prefix that indicates a local
// filesystem path rather than an ALZ Library member path.
func looksLikeLocalPath(p string) bool {
if strings.HasPrefix(p, ".") || strings.HasPrefix(p, "/") || strings.HasPrefix(p, `\`) {
return true
}
// Windows drive letter, e.g. "C:" or "c:".
if len(p) >= 2 && p[1] == ':' {
c := p[0]
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
return true
}
}

return false
}
140 changes: 140 additions & 0 deletions metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,143 @@ func TestNewCustomLibraryReferenceFromFS(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, fs.FS(memfs), got)
}

// TestNewAlzLibraryReferenceFromString verifies parsing of "<path>@<ref>" strings.
func TestNewAlzLibraryReferenceFromString(t *testing.T) {
t.Parallel()

cases := []struct {
name string
input string
wantErr bool
wantPath string
wantRef string
}{
{name: "valid", input: "platform/alz@2026.01.3", wantPath: "platform/alz", wantRef: "2026.01.3"},
{name: "single segment path", input: "alz@2024.07.01", wantPath: "alz", wantRef: "2024.07.01"},
{name: "no separator", input: "platform/alz", wantErr: true},
{name: "empty ref", input: "platform/alz@", wantErr: true},
{name: "empty path", input: "@2026.01.3", wantErr: true},
{name: "empty string", input: "", wantErr: true},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ref, err := NewAlzLibraryReferenceFromString(tc.input)
if tc.wantErr {
require.Error(t, err)
assert.Nil(t, ref)

return
}

require.NoError(t, err)
require.NotNil(t, ref)
assert.Equal(t, tc.wantPath, ref.Path())
assert.Equal(t, tc.wantRef, ref.Ref())
// Round-trip via String().
assert.Equal(t, tc.input, ref.String())
})
}
}

// TestNewLibraryReference verifies the universal constructor's detection of
// ALZ Library references vs. custom (URL or local path) references.
func TestNewLibraryReference(t *testing.T) {
t.Parallel()

cases := []struct {
name string
input string
wantAlz bool
wantPath string
wantRef string // only for ALZ refs
wantCustS string // only for custom refs (String())
}{
{
name: "alz reference",
input: "platform/alz@2026.01.3",
wantAlz: true,
wantPath: "platform/alz",
wantRef: "2026.01.3",
},
{
name: "alz reference single segment",
input: "alz@2024.07.01",
wantAlz: true,
wantPath: "alz",
wantRef: "2024.07.01",
},
{
name: "local relative path",
input: "./mylib",
wantAlz: false,
wantCustS: "./mylib",
},
{
name: "local relative path with at sign",
input: "./mylib@dev",
wantAlz: false,
wantCustS: "./mylib@dev",
},
{
name: "absolute path",
input: "/tmp/lib",
wantAlz: false,
wantCustS: "/tmp/lib",
},
{
name: "windows-style path",
input: `\\share\lib`,
wantAlz: false,
wantCustS: `\\share\lib`,
},
{
name: "windows drive path with at sign",
input: `C:\libs\mylib@dev`,
wantAlz: false,
wantCustS: `C:\libs\mylib@dev`,
},
{
name: "windows drive path lowercase with at sign",
input: `d:/libs/mylib@dev`,
wantAlz: false,
wantCustS: `d:/libs/mylib@dev`,
},
{
name: "no separator",
input: "platform/alz",
wantAlz: false,
wantCustS: "platform/alz",
},
{
name: "trailing at sign with empty ref",
input: "platform/alz@",
wantAlz: false,
wantCustS: "platform/alz@",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got := NewLibraryReference(tc.input)
if tc.wantAlz {
ref, ok := got.(*AlzLibraryReference)
if assert.True(t, ok, "expected *AlzLibraryReference, got %T", got) {
assert.Equal(t, tc.wantPath, ref.Path())
assert.Equal(t, tc.wantRef, ref.Ref())
}

return
}

_, ok := got.(*CustomLibraryReference)
assert.True(t, ok, "expected *CustomLibraryReference, got %T", got)
assert.Equal(t, tc.wantCustS, got.String())
})
}
}
Loading