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
25 changes: 18 additions & 7 deletions cmd/alzlibtool/command/cache/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ A --library value may be either a local path (e.g. ./mylib) or an ALZ Library re
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 --library-overwrite-enabled to apply the current library-overwrite behavior while combining
libraries. Policy assignments, role definitions, archetypes, architectures and policy default
values from later libraries replace those of earlier libraries in full, they are not merged
field by field. Policy definitions and policy set definitions are not replaced: the earlier
library's version is kept and the flag only suppresses the duplicate error. Duplicate archetype
override names remain an error regardless of this flag.

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,
Expand Down Expand Up @@ -88,6 +91,13 @@ to update a cache in-place.`,
os.Exit(1)
}

if libraryOverwriteEnabled {
// stderr only, so verbose stdout stays scriptable.
cmd.PrintErrln(
"Warning: policy (set) definition redefinitions retain the earlier library's version, " +
"and duplicate archetype override names remain an error.")
}

// 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 @@ -264,8 +274,9 @@ func init() {
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.")
"Apply the current library-overwrite behavior while combining libraries. Policy "+
"assignments, role definitions, archetypes, architectures and policy default values from "+
"later libraries are replaced in full, not merged field by field. Policy (set) definitions "+
"keep the earlier library's version; for those the flag only suppresses the duplicate "+
"error. Requires --library and --architecture.")
}
35 changes: 33 additions & 2 deletions cmd/alzlibtool/command/check/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,35 @@ import (
var libraryCmd = cobra.Command{
Use: "library [flags] dir",
Short: "Perform operations on an alzlib library member.",
Long: `Primarily used a a tool to check the validity of a library member.`,
Args: cobra.ExactArgs(1),
Long: `Primarily used as a tool to check the validity of a library member.

Use --library-overwrite-enabled when this library member intentionally redefines assets
that are also provided by its dependencies. Without it, the check fails during library
initialization with an "already exists in the library" error and no checks are run.

Assets are replaced in full, they are not merged field by field, so the redefining file
must contain the complete asset. Policy assignments, role definitions, archetypes,
architectures and policy default values are replaced by this member's version. Policy
definitions and policy set definitions are not: for those the flag only suppresses the
duplicate error and the dependency version is kept. Duplicate archetype override names
remain an error regardless of this flag.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
az := alzlib.NewAlzLib(nil)

libraryOverwriteEnabled, err := cmd.Flags().GetBool("library-overwrite-enabled")
if err != nil {
cmd.PrintErrf(
"%s could not get library-overwrite-enabled flag: %v\n",
cmd.ErrPrefix(),
err,
)
os.Exit(1)
}

// Options are read during Init, so this must be set beforehand.
az.Options.AllowOverwrite = libraryOverwriteEnabled

offline, err := cmd.Flags().GetBool("offline")
if err != nil {
cmd.PrintErrf("%s could not get offline flag: %v\n", cmd.ErrPrefix(), err)
Expand Down Expand Up @@ -104,4 +128,11 @@ func init() {
"Whether to fix any fixable issues (currently only filename issues).")
libraryCmd.Flags().
Bool("offline", false, "Whether to run the checks in offline mode (no Azure calls).")
libraryCmd.Flags().
Bool(
"library-overwrite-enabled", false,
"Allow this library member to redefine assets provided by its dependencies. Policy "+
"assignments, role definitions, archetypes, architectures and policy default values are "+
"replaced in full, not merged field by field. Policy (set) definitions keep the "+
"dependency version; for those the flag only suppresses the duplicate error.")
}
113 changes: 113 additions & 0 deletions cmd/alzlibtool/command/check/library_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package check

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// helperEnvVar, when set, makes the test binary run the library check command instead of the
// tests. This lets the tests observe the os.Exit calls made by the command as an exit code.
const helperEnvVar = "ALZLIBTOOL_CHECK_LIBRARY_HELPER_ARGS"

// helperArgSeparator separates the command arguments passed via helperEnvVar.
// Environment variables cannot contain NUL, and no argument contains a newline.
const helperArgSeparator = "\n"

// Fixtures shared with the generate and document command tests.
const (
overwriteMemberLib = "../testdata/libraryoverwrite/member"
overwritePdMemberLib = "../testdata/libraryoverwrite/pdmember"
standaloneLib = "../testdata/standalone"
)

func TestMain(m *testing.M) {
if rawArgs, ok := os.LookupEnv(helperEnvVar); ok {
// libraryCmd has CheckCmd as its parent, so the whole tree must be executed.
cmd := CheckCmd
cmd.SetArgs(append([]string{"library"}, strings.Split(rawArgs, helperArgSeparator)...))

if err := cmd.ExecuteContext(context.Background()); err != nil {
os.Exit(1)
}

os.Exit(0)
}

os.Exit(m.Run())
}

// runCheckLibrary runs the library check command in a child process and returns its combined
// output and exit code.
func runCheckLibrary(t *testing.T, args ...string) (string, int) {
t.Helper()

cmd := exec.Command(os.Args[0])

cmd.Env = append(
os.Environ(),
fmt.Sprintf("%s=%s", helperEnvVar, strings.Join(args, helperArgSeparator)),
)

out, err := cmd.CombinedOutput()
if err == nil {
return string(out), 0
}

var exitErr *exec.ExitError

require.ErrorAs(t, err, &exitErr, "unexpected error running check library: %v", err)

return string(out), exitErr.ExitCode()
}

func Test_libraryCmd_overwriteFlagDefault(t *testing.T) {
f := libraryCmd.Flags().Lookup("library-overwrite-enabled")
require.NotNil(t, f, "library-overwrite-enabled flag should be registered")
assert.Equal(t, "false", f.DefValue)
}

func Test_libraryCmd_help(t *testing.T) {
out, code := runCheckLibrary(t, "--help")
assert.Equal(t, 0, code)
assert.Contains(t, out, "--library-overwrite-enabled")
}

func Test_libraryCmd_overwriteDisabledByDefault(t *testing.T) {
out, code := runCheckLibrary(t, overwriteMemberLib, "--offline")
assert.Equal(t, 1, code)
assert.Contains(t, out, "already exists in the library")
}

// Test_libraryCmd_overwriteEnabled asserts that the member version wins: the member defaults
// require memberParameter, which only exists on the definition referenced by the member version
// of shared-pa, so passing the defaults check proves the dependency version was replaced.
func Test_libraryCmd_overwriteEnabled(t *testing.T) {
out, code := runCheckLibrary(t, overwriteMemberLib, "--offline", "--library-overwrite-enabled")
assert.Equal(t, 0, code, "output: %s", out)
assert.NotContains(t, out, "already exists in the library")
}

func Test_libraryCmd_noConflictWithoutFlag(t *testing.T) {
out, code := runCheckLibrary(t, standaloneLib, "--offline")
assert.Equal(t, 0, code, "output: %s", out)
}

// Test_libraryCmd_policyDefinitionOverrideNotApplied pins current engine behaviour: for policy
// (set) definitions the flag only suppresses the duplicate error, the dependency version is
// kept. The pdmember version of dependency-pd adds the parameter its defaults require, so the
// defaults check failing proves the member version was discarded.
func Test_libraryCmd_policyDefinitionOverrideNotApplied(t *testing.T) {
out, code := runCheckLibrary(t, overwritePdMemberLib, "--offline", "--library-overwrite-enabled")
assert.Equal(t, 1, code)
assert.Contains(t, out, "not present in the referenced definition")
}
38 changes: 35 additions & 3 deletions cmd/alzlibtool/command/document/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ import (
var documentLibraryBaseCmd = cobra.Command{
Use: "library path",
Short: "Generates documentation for the supplied library path.",
Long: `Generates documentation for the supplied library path.`,
Args: cobra.ExactArgs(1),
Long: `Generates documentation for the supplied library path.

Use --library-overwrite-enabled when this library member intentionally redefines assets that
are also provided by its dependencies. Without it, documentation generation fails during
library initialization with an "already exists in the library" error.

The generated documentation follows the current overwrite semantics: policy assignments, role
definitions, archetypes, architectures and policy default values are replaced in full by this
member's version. Policy definitions and policy set definitions are not, the dependency
version is kept. Duplicate archetype override names remain an error regardless of this flag.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
thislib := alzlib.NewCustomLibraryReference(args[0])

Expand All @@ -29,10 +38,33 @@ var documentLibraryBaseCmd = cobra.Command{
os.Exit(1)
}

err = doc.AlzlibReadmeMd(cmd.Context(), os.Stdout, alllibs...)
libraryOverwriteEnabled, _ := cmd.Flags().GetBool("library-overwrite-enabled")

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

if libraryOverwriteEnabled {
// stderr only, so the Markdown on stdout stays usable in a pipeline.
cmd.PrintErrln(
"Warning: generated documentation follows the existing overwrite semantics; " +
"policy (set) definition redefinitions retain the dependency version, and " +
"duplicate archetype override names remain an error.")
}

err = doc.AlzlibReadmeMdWithOptions(cmd.Context(), os.Stdout, opts, alllibs...)
if err != nil {
cmd.PrintErrf("%s library documentation error: %v\n", cmd.ErrPrefix(), err)
os.Exit(1)
}
},
}

func init() {
documentLibraryBaseCmd.Flags().
Bool(
"library-overwrite-enabled", false,
"Document the library using the current overwrite semantics. Policy assignments, role "+
"definitions, archetypes, architectures and policy default values are replaced in full, "+
"not merged field by field. Policy (set) definitions keep the dependency version; for "+
"those the flag only suppresses the duplicate error.")
}
96 changes: 96 additions & 0 deletions cmd/alzlibtool/command/document/library_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package document

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// helperEnvVar, when set, makes the test binary run the document library command instead of the
// tests. This lets the tests observe the os.Exit calls made by the command as an exit code.
const helperEnvVar = "ALZLIBTOOL_DOCUMENT_LIBRARY_HELPER_ARGS"

// helperArgSeparator separates the command arguments passed via helperEnvVar.
// Environment variables cannot contain NUL, and no argument contains a newline.
const helperArgSeparator = "\n"

// overwriteMemberLib is shared with the check and generate command tests.
const overwriteMemberLib = "../testdata/libraryoverwrite/member"

func TestMain(m *testing.M) {
if rawArgs, ok := os.LookupEnv(helperEnvVar); ok {
// documentLibraryBaseCmd has DocumentBaseCmd as its parent, so the whole tree must run.
cmd := DocumentBaseCmd
cmd.SetArgs(append([]string{"library"}, strings.Split(rawArgs, helperArgSeparator)...))

if err := cmd.ExecuteContext(context.Background()); err != nil {
os.Exit(1)
}

os.Exit(0)
}

os.Exit(m.Run())
}

// runDocumentLibrary runs the command in a child process and returns stdout, stderr and the
// exit code separately, so that the Markdown artifact can be checked independently of warnings.
func runDocumentLibrary(t *testing.T, args ...string) (string, string, int) {
t.Helper()

cmd := exec.Command(os.Args[0])

cmd.Env = append(
os.Environ(),
fmt.Sprintf("%s=%s", helperEnvVar, strings.Join(args, helperArgSeparator)),
)

var stdout, stderr strings.Builder

cmd.Stdout = &stdout
cmd.Stderr = &stderr

err := cmd.Run()
if err == nil {
return stdout.String(), stderr.String(), 0
}

var exitErr *exec.ExitError

require.ErrorAs(t, err, &exitErr, "unexpected error running document library: %v", err)

return stdout.String(), stderr.String(), exitErr.ExitCode()
}

func Test_documentLibraryCmd_overwriteFlagDefault(t *testing.T) {
f := documentLibraryBaseCmd.Flags().Lookup("library-overwrite-enabled")
require.NotNil(t, f, "library-overwrite-enabled flag should be registered")
assert.Equal(t, "false", f.DefValue)
}

func Test_documentLibraryCmd_overwriteDisabledByDefault(t *testing.T) {
_, stderr, code := runDocumentLibrary(t, overwriteMemberLib)

assert.Equal(t, 1, code)
assert.Contains(t, stderr, "already exists in the library")
}

// Test_documentLibraryCmd_overwriteEnabled asserts that the member archetype wins, and that the
// warning goes to stderr so the Markdown on stdout stays usable.
func Test_documentLibraryCmd_overwriteEnabled(t *testing.T) {
stdout, stderr, code := runDocumentLibrary(t, overwriteMemberLib, "--library-overwrite-enabled")

require.Equal(t, 0, code, "stderr: %s", stderr)
assert.Contains(t, stdout, "member-only-pa")
assert.Contains(t, stderr, "Warning:")
assert.NotContains(t, stdout, "Warning:")
}
Loading
Loading