diff --git a/defaultconfigs/defaultconfigs.go b/defaultconfigs/defaultconfigs.go index a23f6a51..1445f1c1 100644 --- a/defaultconfigs/defaultconfigs.go +++ b/defaultconfigs/defaultconfigs.go @@ -18,6 +18,10 @@ const embedfsRootDir = "/content" // Name of the root default configuration file that should be loaded from these default files. const rootDefaultConfigFilename = "defaults.toml" +// DefaultRpmRepoSetTemplateName is the name of the built-in standard Azure +// Linux rpm-repo-set-template defined in `content/defaults.toml`. +const DefaultRpmRepoSetTemplateName = "azl-standard" + // The embedded filesystem containing compiled-in default configuration files. This configuration // defines distro-level defaults for consistency. // diff --git a/docs/user/reference/cli/azldev.md b/docs/user/reference/cli/azldev.md index 00c8b4db..a9072dc0 100644 --- a/docs/user/reference/cli/azldev.md +++ b/docs/user/reference/cli/azldev.md @@ -41,5 +41,6 @@ lives), or use -C to point to one. * [azldev image](azldev_image.md) - Manage Azure Linux images * [azldev package](azldev_package.md) - Manage binary package configuration * [azldev project](azldev_project.md) - Manage Azure Linux projects +* [azldev repo](azldev_repo.md) - Inspect and manage RPM repositories * [azldev version](azldev_version.md) - Print the CLI version diff --git a/docs/user/reference/cli/azldev_repo.md b/docs/user/reference/cli/azldev_repo.md new file mode 100644 index 00000000..e9132d0b --- /dev/null +++ b/docs/user/reference/cli/azldev_repo.md @@ -0,0 +1,41 @@ + + +## azldev repo + +Inspect and manage RPM repositories + +### Synopsis + +Inspect and manage RPM repositories. + +Subcommands operate over upstream RPM repos described by an +rpm-repo-set-template (e.g. the built-in "azl-standard" layout) expanded +under one or more URL prefixes. + +### Options + +``` + -h, --help help for repo +``` + +### Options inherited from parent commands + +``` + -y, --accept-all accept all prompts + --color mode output colorization mode {always, auto, never} (default auto) + --config-file stringArray additional TOML config file(s) to merge (may be repeated) + -n, --dry-run dry run only (do not take action) + --network-retries int maximum number of attempts for network operations (minimum 1) (default 3) + --no-default-config disable default configuration + -O, --output-format fmt output format {csv, json, markdown, table} (default table) + --permissive-config do not fail on unknown fields in TOML config files + -C, --project string path to Azure Linux project + -q, --quiet only enable minimal output + -v, --verbose enable verbose output +``` + +### SEE ALSO + +* [azldev](azldev.md) - 🐧 Azure Linux Dev Tool +* [azldev repo query](azldev_repo_query.md) - Run dnf against auto-discovered RPM repos + diff --git a/docs/user/reference/cli/azldev_repo_query.md b/docs/user/reference/cli/azldev_repo_query.md new file mode 100644 index 00000000..f45a51ce --- /dev/null +++ b/docs/user/reference/cli/azldev_repo_query.md @@ -0,0 +1,81 @@ + + +## azldev repo query + +Run dnf against auto-discovered RPM repos + +### Synopsis + +Thin wrapper around dnf that auto-discovers RPM repos and execs into +dnf with the resolved repos wired up via --repofrompath / --enablerepo. + +Two selection modes, mutually exclusive: + + --repo-prefix URL [--repo-prefix URL]... + URL mode. Each URL is expanded against an rpm-repo-set-template + (--template, default "azl-standard") into one sub-repo per template + row, fanned out per --arch where the row's subpath contains $basearch. + + --version VER [--use-case rpm-build|image-build] + Project-config mode. Resolves the inputs list of the default distro's + VER version (use-case defaults to "rpm-build"). Gpg-keys + and per-repo arch allowlists come from [resources.rpm-repo-sets.*] / + [resources.rpm-repos.*]. --arch defaults to x86_64+aarch64 (each + repo is still filtered by its declared arches). --no-debuginfo / + --no-srpms drop sub-repos by their declared kind. --template is not + used. + +Unreachable sub-repos are tolerated via dnf's per-repo +skip_if_unavailable=1 setopt; dnf itself logs and skips ones that fail to +load. + +All positional arguments are passed verbatim to dnf. Use `--` to separate +azldev flags from dnf flags. + +Examples: + # URL-mode query against a published tree + azldev repo query --repo-prefix=https://packages.microsoft.com/azurelinux/4.0/beta -- repoquery --available bash + + # whatever the current project's default distro 4.0-stage2 build consumes + azldev repo query --version 4.0-stage2 -- list --available kernel + + # image-build inputs instead of rpm-build + azldev repo query --version 4.0-stage2 --use-case image-build -- repolist + +``` +azldev repo query [flags] -- +``` + +### Options + +``` + --arch strings comma-separated arches to expand $basearch over (default [x86_64,aarch64]) + -h, --help help for query + --no-debuginfo drop sub-repos whose kind is debug + --no-srpms drop sub-repos whose kind is source + --repo-prefix stringArray layout prefix (http://, https://, or file:// URL); may be repeated + --template string name of the rpm-repo-set-template to expand each --repo-prefix against (default "azl-standard") + --use-case string which inputs list to consult in --version mode: rpm-build or image-build (default "rpm-build") + --version string resolve repos from the default distro's [distros..versions..inputs] list (mutually exclusive with --repo-prefix and --template) +``` + +### Options inherited from parent commands + +``` + -y, --accept-all accept all prompts + --color mode output colorization mode {always, auto, never} (default auto) + --config-file stringArray additional TOML config file(s) to merge (may be repeated) + -n, --dry-run dry run only (do not take action) + --network-retries int maximum number of attempts for network operations (minimum 1) (default 3) + --no-default-config disable default configuration + -O, --output-format fmt output format {csv, json, markdown, table} (default table) + --permissive-config do not fail on unknown fields in TOML config files + -C, --project string path to Azure Linux project + -q, --quiet only enable minimal output + -v, --verbose enable verbose output +``` + +### SEE ALSO + +* [azldev repo](azldev_repo.md) - Inspect and manage RPM repositories + diff --git a/internal/app/azldev/cmds/repo/query.go b/internal/app/azldev/cmds/repo/query.go new file mode 100644 index 00000000..4b82ee76 --- /dev/null +++ b/internal/app/azldev/cmds/repo/query.go @@ -0,0 +1,547 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package repo + +import ( + "errors" + "fmt" + "log/slog" + "os" + "os/exec" + + "github.com/microsoft/azure-linux-dev-tools/defaultconfigs" + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev" + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" + "github.com/microsoft/azure-linux-dev-tools/internal/repo/repolayout" + "github.com/microsoft/azure-linux-dev-tools/internal/utils/prereqs" + "github.com/spf13/cobra" +) + +// DnfBinary is the underlying system binary the wrapper invokes. +const DnfBinary = "dnf" + +// QueryOptions are the CLI flags for `azldev repo query`. +type QueryOptions struct { + RepoPrefixes []string + Template string + Arches []string + NoDebuginfo bool + NoSRPMs bool + Version string + UseCase string +} + +func queryOnAppInit(_ *azldev.App, parentCmd *cobra.Command) { + parentCmd.AddCommand(NewQueryCmd()) +} + +// NewQueryCmd constructs the cobra command for `azldev repo query`. +func NewQueryCmd() *cobra.Command { + var options QueryOptions + + cmd := &cobra.Command{ + Use: "query [flags] -- ", + Short: "Run dnf against auto-discovered RPM repos", + Long: `Thin wrapper around dnf that auto-discovers RPM repos and execs into +dnf with the resolved repos wired up via --repofrompath / --enablerepo. + +Two selection modes, mutually exclusive: + + --repo-prefix URL [--repo-prefix URL]... + URL mode. Each URL is expanded against an rpm-repo-set-template + (--template, default "` + defaultconfigs.DefaultRpmRepoSetTemplateName + `") into one sub-repo per template + row, fanned out per --arch where the row's subpath contains $basearch. + + --version VER [--use-case rpm-build|image-build] + Project-config mode. Resolves the inputs list of the default distro's + VER version (use-case defaults to "` + projectconfig.UseCaseRPMBuild + `"). Gpg-keys + and per-repo arch allowlists come from [resources.rpm-repo-sets.*] / + [resources.rpm-repos.*]. --arch defaults to x86_64+aarch64 (each + repo is still filtered by its declared arches). --no-debuginfo / + --no-srpms drop sub-repos by their declared kind. --template is not + used. + +Unreachable sub-repos are tolerated via dnf's per-repo +skip_if_unavailable=1 setopt; dnf itself logs and skips ones that fail to +load. + +All positional arguments are passed verbatim to dnf. Use ` + "`--`" + ` to separate +azldev flags from dnf flags. + +Examples: + # URL-mode query against a published tree + azldev repo query --repo-prefix=https://packages.microsoft.com/azurelinux/4.0/beta -- repoquery --available bash + + # whatever the current project's default distro 4.0-stage2 build consumes + azldev repo query --version 4.0-stage2 -- list --available kernel + + # image-build inputs instead of rpm-build + azldev repo query --version 4.0-stage2 --use-case image-build -- repolist`, + } + + cmd.RunE = azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) { + return nil, RunQuery(env, &options, args) + }) + + cmd.Flags().SetInterspersed(false) + + cmd.Flags().StringArrayVar(&options.RepoPrefixes, "repo-prefix", nil, + "layout prefix (http://, https://, or file:// URL); may be repeated") + cmd.Flags().StringVar(&options.Template, "template", defaultconfigs.DefaultRpmRepoSetTemplateName, + "name of the rpm-repo-set-template to expand each --repo-prefix against") + cmd.Flags().StringSliceVar(&options.Arches, "arch", repolayout.DefaultArches, + "comma-separated arches to expand $basearch over") + cmd.Flags().BoolVar(&options.NoDebuginfo, "no-debuginfo", false, + "drop sub-repos whose kind is debug") + cmd.Flags().BoolVar(&options.NoSRPMs, "no-srpms", false, + "drop sub-repos whose kind is source") + cmd.Flags().StringVar(&options.Version, "version", "", + "resolve repos from the default distro's [distros..versions..inputs] list "+ + "(mutually exclusive with --repo-prefix and --template)") + cmd.Flags().StringVar(&options.UseCase, "use-case", projectconfig.UseCaseRPMBuild, + "which inputs list to consult in --version mode: "+ + projectconfig.UseCaseRPMBuild+" or "+projectconfig.UseCaseImageBuild) + + cmd.MarkFlagsMutuallyExclusive("repo-prefix", "version") + cmd.MarkFlagsMutuallyExclusive("template", "version") + cmd.MarkFlagsOneRequired("repo-prefix", "version") + + return cmd +} + +// RunQuery is the entry point for `azldev repo query`. It resolves the +// candidate sub-repos and runs `dnf` with them wired up via +// --repofrompath / --enablerepo plus skip_if_unavailable=1 — letting dnf +// itself log and tolerate sub-repos that turn out to be missing. +func RunQuery(env *azldev.Env, options *QueryOptions, dnfArgs []string) error { + if err := prereqs.RequireExecutable(env, DnfBinary, &prereqs.PackagePrereq{ + AzureLinuxPackages: []string{DnfBinary}, + FedoraPackages: []string{DnfBinary}, + }); err != nil { + return fmt.Errorf("%s is required to query RPM repos:\n%w", DnfBinary, err) + } + + groups, prefixes, err := buildCandidates(env, options) + if err != nil { + return err + } + + groups = filterGroupsByKind(groups, options) + repos := logAndFlatten(groups, prefixes) + + if len(repos) == 0 { + return errors.New("no sub-repos remain after applying filters") + } + + return runDNF(env, buildDNFArgv(repos, dnfArgs)) +} + +// logAndFlatten emits a per-group discovery log and returns the concatenated +// candidate list. Each row's URL is logged so a user reading the build output +// can see exactly what was handed to dnf, while dnf itself decides which +// repos actually load. +func logAndFlatten(groups [][]repolayout.InputRepo, prefixes []string) []repolayout.InputRepo { + total := 0 + for _, g := range groups { + total += len(g) + } + + out := make([]repolayout.InputRepo, 0, total) + + logRepos := func(group []repolayout.InputRepo) { + for _, repo := range group { + slog.Info(" wiring sub-repo", "id", repo.RepoID, "url", repo.URL) + } + } + + if len(prefixes) == 0 { + slog.Info("Resolving repos from project config", "count", total) + + for _, group := range groups { + logRepos(group) + out = append(out, group...) + } + + return out + } + + for pIdx, prefix := range prefixes { + slog.Info("Discovering repos under prefix", "prefix", prefix) + + if len(groups[pIdx]) == 0 { + slog.Warn("No sub-repos under prefix", "prefix", prefix) + } + + logRepos(groups[pIdx]) + out = append(out, groups[pIdx]...) + } + + return out +} + +// runDNF runs the assembled dnf invocation via the env's command factory +// (backed by [externalcmd]) so the call is observable / dry-run-aware. +func runDNF(env *azldev.Env, argv []string) error { + rawCmd := exec.CommandContext(env, argv[0], argv[1:]...) + rawCmd.Stdin = os.Stdin + rawCmd.Stdout = os.Stdout + rawCmd.Stderr = os.Stderr + + cmd, err := env.Command(rawCmd) + if err != nil { + return fmt.Errorf("failed to create %s command:\n%w", DnfBinary, err) + } + + if err := cmd.Run(env); err != nil { + // Match the original syscall.Exec semantics by propagating dnf's exit + // code verbatim (e.g., 100 for `check-update` with updates available) + // instead of letting cobra collapse it to 1. + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + os.Exit(exitErr.ExitCode()) + } + + return fmt.Errorf("%s run failed:\n%w", DnfBinary, err) + } + + return nil +} + +// buildCandidates picks the input-list builder based on which mode the user +// selected and returns (groups, prefixesForLogging). groups is one +// [repolayout.InputRepo] slice per --repo-prefix (preserving user order) in +// prefix mode, or a single slice in --version mode. prefixesForLogging is +// non-empty only in --repo-prefix mode so logAndFlatten can label its +// per-group output. +func buildCandidates(env *azldev.Env, options *QueryOptions) ([][]repolayout.InputRepo, []string, error) { + if options.Version != "" { + repos, err := reposFromVersion(env, options) + if err != nil { + return nil, nil, err + } + + return [][]repolayout.InputRepo{repos}, nil, nil + } + + templateName := options.Template + if templateName == "" { + templateName = defaultconfigs.DefaultRpmRepoSetTemplateName + } + + tmpl, err := repolayout.ResolveTemplate( + env.Config().Resources.RpmRepoSetTemplates, templateName) + if err != nil { + return nil, nil, fmt.Errorf("failed to resolve template:\n%w", err) + } + + groups, err := buildInputRepos(options, templateName, tmpl, options.Arches) + if err != nil { + return nil, nil, err + } + + return groups, options.RepoPrefixes, nil +} + +// reposFromVersion resolves a distro version's inputs into the flat +// [repolayout.InputRepo] list the rest of the pipeline expects. +func reposFromVersion(env *azldev.Env, options *QueryOptions) ([]repolayout.InputRepo, error) { + useCase := options.UseCase + if useCase != projectconfig.UseCaseRPMBuild && useCase != projectconfig.UseCaseImageBuild { + return nil, fmt.Errorf("--use-case %#q is invalid (want %q or %q)", + useCase, projectconfig.UseCaseRPMBuild, projectconfig.UseCaseImageBuild) + } + + cfg := env.Config() + version := options.Version + + distroName := cfg.Project.DefaultDistro.Name + if distroName == "" { + return nil, errors.New("--version requires `project.default-distro.name` to be set in project config") + } + + names, err := resolveVersionRepoNames(cfg, distroName, version, useCase) + if err != nil { + return nil, err + } + + effective, err := cfg.Resources.EffectiveRpmRepos() + if err != nil { + return nil, fmt.Errorf("resolving rpm-repos:\n%w", err) + } + + kinds := rpmRepoKindMap(&cfg.Resources) + + return materializeVersionRepos(names, effective, kinds, options.Arches, options, distroName, version) +} + +// rpmRepoKindMap reproduces the name -> SubrepoKind mapping that +// [projectconfig.ResourcesConfig.EffectiveRpmRepos] discards. Explicit +// rpm-repos are treated as Binary; set-expanded entries take their kind +// from the template's SubrepoSpec. +func rpmRepoKindMap(resources *projectconfig.ResourcesConfig) map[string]projectconfig.SubrepoKind { + kinds := make(map[string]projectconfig.SubrepoKind) + if resources == nil { + return kinds + } + + for name := range resources.RpmRepos { + kinds[name] = projectconfig.SubrepoKindBinary + } + + for _, set := range resources.RpmRepoSets { + tmpl, ok := resources.RpmRepoSetTemplates[set.Template] + if !ok { + continue + } + + allow := map[string]struct{}{} + for _, subName := range set.Subrepos { + allow[subName] = struct{}{} + } + + for _, sub := range tmpl.Subrepos { + if len(allow) > 0 { + if _, ok := allow[sub.Name]; !ok { + continue + } + } + + kinds[set.NamePrefix+sub.Name] = sub.Kind.Default() + } + } + + return kinds +} + +// resolveVersionRepoNames returns the ordered list of rpm-repo names declared +// for (distroName, version, useCase) after set expansion. +func resolveVersionRepoNames( + cfg *projectconfig.ProjectConfig, + distroName, version, useCase string, +) ([]string, error) { + distro, found := cfg.Distros[distroName] + if !found { + return nil, fmt.Errorf("default distro %#q is not defined under [distros]", distroName) + } + + versionDef, found := distro.Versions[version] + if !found { + return nil, fmt.Errorf("distro %#q has no version %#q", distroName, version) + } + + var ( + names []string + err error + ) + + switch useCase { + case projectconfig.UseCaseRPMBuild: + names, err = versionDef.EffectiveRpmBuildRepos(&cfg.Resources) + case projectconfig.UseCaseImageBuild: + names, err = versionDef.EffectiveImageBuildRepos(&cfg.Resources) + } + + if err != nil { + return nil, fmt.Errorf("resolving %s inputs for %s/%s:\n%w", useCase, distroName, version, err) + } + + if len(names) == 0 { + return nil, fmt.Errorf("%s/%s declares no %s inputs", distroName, version, useCase) + } + + return names, nil +} + +// materializeVersionRepos turns rpm-repo names into the flat InputRepo list, +// expanding over arches and applying --no-debuginfo / --no-srpms via kinds. +// Per-repo arch allowlists ([RpmRepoResource.Arches]) drop arches a repo +// doesn't publish; metalink-only repos are rejected. +func materializeVersionRepos( + names []string, + effective map[string]projectconfig.RpmRepoResource, + kinds map[string]projectconfig.SubrepoKind, + arches []string, + options *QueryOptions, + distroName, version string, +) ([]repolayout.InputRepo, error) { + out := make([]repolayout.InputRepo, 0, len(names)*len(arches)) + + for _, name := range names { + repo, found := effective[name] + if !found { + return nil, fmt.Errorf("rpm-repo %#q referenced by %s/%s inputs is not defined", + name, distroName, version) + } + + if repo.BaseURI == "" { + return nil, fmt.Errorf( + "rpm-repo %#q has no base-uri (metalink-only repos are not supported by `repo query`)", + name) + } + + kind := kinds[name].Default() + if options.NoDebuginfo && kind == projectconfig.SubrepoKindDebug { + continue + } + + if options.NoSRPMs && kind == projectconfig.SubrepoKindSource { + continue + } + + gpgKey := "" + if !repo.DisableGPGCheck { + gpgKey = repo.GPGKey + } + + for _, arch := range arches { + if !repo.IsAvailableForArch(arch) { + continue + } + + out = append(out, repolayout.InputRepo{ + RepoID: versionRepoID(name, arches, arch), + URL: repolayout.SubstituteBasearch(repo.BaseURI, arch), + Arch: arch, + GPGKey: gpgKey, + }) + } + } + + // DedupInputRepos collapses entries with identical URLs. That happens + // naturally for source/SRPM subrepos whose subpath has no `$basearch` — + // the per-arch fan-out above produces N identical URLs, and we want only + // one row in the final dnf invocation. + return repolayout.DedupInputRepos(out), nil +} + +// versionRepoID keeps the canonical resource id when only one arch was +// requested (matches the name the user typed in TOML) and appends the arch +// otherwise so per-arch dnf repo ids stay unique. The decision is based on +// the *requested* arch list, not how many survive per-repo filtering — that +// keeps the id stable when the user re-runs with the same flags even if +// some repos drop one arch. +func versionRepoID(name string, arches []string, arch string) string { + if len(arches) > 1 { + return name + "-" + arch + } + + return name +} + +// buildInputRepos normalizes each --repo-prefix, expands it against tmpl, and +// returns one [repolayout.InputRepo] slice per prefix (in user order). The +// dnf repo id is stamped here so downstream stages don't need to know the +// prefix index: the per-prefix suffix is added only when multiple prefixes +// are in play so ids stay unique across them. +func buildInputRepos( + options *QueryOptions, + templateName string, + tmpl projectconfig.RpmRepoSetTemplate, + arches []string, +) ([][]repolayout.InputRepo, error) { + total := len(options.RepoPrefixes) + groups := make([][]repolayout.InputRepo, 0, total) + + for idx, prefix := range options.RepoPrefixes { + normalized, err := repolayout.NormalizePrefix(prefix) + if err != nil { + return nil, fmt.Errorf("--repo-prefix %#q: %w", prefix, err) + } + + expanded := repolayout.ExpandTemplate(normalized, templateName, tmpl, arches) + for i := range expanded { + expanded[i].RepoID = prefixModeRepoID(expanded[i], idx+1, total) + } + + groups = append(groups, repolayout.DedupInputRepos(expanded)) + } + + return groups, nil +} + +// prefixModeRepoID mints the dnf repo id for a prefix-mode slot. The base +// form is `azl-`; the arch is appended when the row was fanned out +// per arch (so per-arch slots don't collide); and the 1-based prefix index +// is appended only when multiple --repo-prefix values were supplied so ids +// stay unique across prefixes. +func prefixModeRepoID(repo repolayout.InputRepo, prefixIdx, totalPrefixes int) string { + repoID := "azl-" + repo.SubrepoName + if repo.Arch != "" { + repoID = repoID + "-" + repo.Arch + } + + if totalPrefixes > 1 { + repoID = fmt.Sprintf("%s-%d", repoID, prefixIdx) + } + + return repoID +} + +// filterGroupsByKind drops debug/source rows per --no-debuginfo / --no-srpms, +// preserving the per-prefix grouping (and the empty-group warning that +// logAndFlatten emits). +func filterGroupsByKind( + groups [][]repolayout.InputRepo, options *QueryOptions, +) [][]repolayout.InputRepo { + if !options.NoDebuginfo && !options.NoSRPMs { + return groups + } + + out := make([][]repolayout.InputRepo, len(groups)) + + for gIdx, group := range groups { + kept := make([]repolayout.InputRepo, 0, len(group)) + + for _, repo := range group { + switch repo.Kind { + case projectconfig.SubrepoKindDebug: + if options.NoDebuginfo { + continue + } + case projectconfig.SubrepoKindSource: + if options.NoSRPMs { + continue + } + case projectconfig.SubrepoKindBinary: + // keep + } + + kept = append(kept, repo) + } + + out[gIdx] = kept + } + + return out +} + +// buildDNFArgv builds the argv passed to dnf. The first element is the +// program name ("dnf"); the rest disables any host-configured repos, forces +// a metadata refresh, wires up one --repofrompath / --enablerepo pair per +// candidate slot (with skip_if_unavailable=1 so dnf tolerates ones that +// don't actually exist), and finally appends the user's passthrough. +func buildDNFArgv(repos []repolayout.InputRepo, userArgs []string) []string { + argv := make([]string, 0, 3+len(repos)*7+len(userArgs)) + argv = append(argv, DnfBinary, "--disablerepo=*", "--refresh") + + for _, dnfRepo := range repos { + repoID := dnfRepo.RepoID + argv = append(argv, + "--repofrompath", repoID+","+dnfRepo.URL, + "--enablerepo", repoID, + "--setopt="+repoID+".skip_if_unavailable=1", + ) + + if dnfRepo.GPGKey != "" { + argv = append(argv, + "--setopt="+repoID+".gpgkey="+dnfRepo.GPGKey, + "--setopt="+repoID+".gpgcheck=1", + ) + } + } + + argv = append(argv, userArgs...) + + return argv +} diff --git a/internal/app/azldev/cmds/repo/query_internal_test.go b/internal/app/azldev/cmds/repo/query_internal_test.go new file mode 100644 index 00000000..25827eda --- /dev/null +++ b/internal/app/azldev/cmds/repo/query_internal_test.go @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package repo + +import ( + "testing" + + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" + "github.com/microsoft/azure-linux-dev-tools/internal/repo/repolayout" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVersionRepoID(t *testing.T) { + t.Parallel() + + assert.Equal(t, "azl4-beta-base", versionRepoID("azl4-beta-base", []string{"x86_64"}, "x86_64")) + assert.Equal(t, "azl4-beta-base-x86_64", + versionRepoID("azl4-beta-base", []string{"x86_64", "aarch64"}, "x86_64")) +} + +func TestMaterializeVersionRepos_ArchAndKindFilters(t *testing.T) { + t.Parallel() + + effective := map[string]projectconfig.RpmRepoResource{ + "base": {BaseURI: "https://example.com/base/$basearch", Arches: []string{"x86_64"}}, + "debug": {BaseURI: "https://example.com/debug/$basearch"}, + "source": {BaseURI: "https://example.com/source"}, + } + kinds := map[string]projectconfig.SubrepoKind{ + "base": projectconfig.SubrepoKindBinary, + "debug": projectconfig.SubrepoKindDebug, + "source": projectconfig.SubrepoKindSource, + } + + out, err := materializeVersionRepos( + []string{"base", "debug", "source"}, + effective, kinds, + []string{"x86_64", "aarch64"}, + &QueryOptions{NoDebuginfo: true}, + "azl", "4.0", + ) + require.NoError(t, err) + + // base: only x86_64 (allowlist drops aarch64). + // debug: dropped by NoDebuginfo. + // source: subpath has no $basearch, so per-arch fan-out yields two + // identical URLs that DedupInputRepos collapses into one. + urls := make([]string, 0, len(out)) + for _, repo := range out { + urls = append(urls, repo.URL) + } + + assert.ElementsMatch(t, []string{ + "https://example.com/base/x86_64", + "https://example.com/source", + }, urls) +} + +func TestMaterializeVersionRepos_NoSRPMs(t *testing.T) { + t.Parallel() + + out, err := materializeVersionRepos( + []string{"src"}, + map[string]projectconfig.RpmRepoResource{ + "src": {BaseURI: "https://example.com/s"}, + }, + map[string]projectconfig.SubrepoKind{"src": projectconfig.SubrepoKindSource}, + []string{"x86_64"}, + &QueryOptions{NoSRPMs: true}, + "azl", "4.0", + ) + require.NoError(t, err) + assert.Empty(t, out) +} + +func TestMaterializeVersionRepos_GPGKeyForwarded(t *testing.T) { + t.Parallel() + + effective := map[string]projectconfig.RpmRepoResource{ + "signed": {BaseURI: "https://example.com/a", GPGKey: "https://example.com/key.asc"}, + "unsigned": {BaseURI: "https://example.com/b", GPGKey: "https://example.com/k", DisableGPGCheck: true}, + } + + out, err := materializeVersionRepos( + []string{"signed", "unsigned"}, + effective, + map[string]projectconfig.SubrepoKind{}, + []string{"x86_64"}, + &QueryOptions{}, "azl", "4.0", + ) + require.NoError(t, err) + require.Len(t, out, 2) + + byID := map[string]repolayout.InputRepo{} + for _, repo := range out { + byID[repo.RepoID] = repo + } + + assert.Equal(t, "https://example.com/key.asc", byID["signed"].GPGKey) + assert.Empty(t, byID["unsigned"].GPGKey, + "DisableGPGCheck should suppress GPGKey forwarding") +} + +func TestMaterializeVersionRepos_UndefinedRepoErrors(t *testing.T) { + t.Parallel() + + _, err := materializeVersionRepos( + []string{"missing"}, + map[string]projectconfig.RpmRepoResource{}, + map[string]projectconfig.SubrepoKind{}, + []string{"x86_64"}, + &QueryOptions{}, "azl", "4.0", + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing") +} + +func TestMaterializeVersionRepos_MetalinkOnlyRejected(t *testing.T) { + t.Parallel() + + _, err := materializeVersionRepos( + []string{"ml"}, + map[string]projectconfig.RpmRepoResource{"ml": {Metalink: "https://example.com/m"}}, + map[string]projectconfig.SubrepoKind{}, + []string{"x86_64"}, + &QueryOptions{}, "azl", "4.0", + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "metalink-only") +} + +func TestBuildDNFArgv_ForwardsGPGSetopts(t *testing.T) { + t.Parallel() + + argv := buildDNFArgv( + []repolayout.InputRepo{ + {RepoID: "signed", URL: "https://example.com/a", GPGKey: "https://example.com/k"}, + {RepoID: "plain", URL: "https://example.com/b"}, + }, + []string{"repolist"}, + ) + + joined := " " + joinArgs(argv) + " " + assert.Contains(t, joined, " --setopt=signed.gpgkey=https://example.com/k ") + assert.Contains(t, joined, " --setopt=signed.gpgcheck=1 ") + assert.NotContains(t, joined, "plain.gpgkey") + assert.NotContains(t, joined, "plain.gpgcheck") +} + +func TestBuildDNFArgv_SkipIfUnavailablePerRepo(t *testing.T) { + t.Parallel() + + argv := buildDNFArgv( + []repolayout.InputRepo{ + {RepoID: "first", URL: "https://example.com/a"}, + {RepoID: "second", URL: "https://example.com/b", GPGKey: "https://example.com/k"}, + }, + []string{"repolist"}, + ) + + joined := " " + joinArgs(argv) + " " + assert.Contains(t, joined, " --setopt=first.skip_if_unavailable=1 ") + assert.Contains(t, joined, " --setopt=second.skip_if_unavailable=1 ") +} + +func TestNewQueryCmd_TemplateVersionMutuallyExclusive(t *testing.T) { + t.Parallel() + + cmd := NewQueryCmd() + cmd.SetArgs([]string{"--version", "4.0", "--template", "azl-standard"}) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "template") + assert.Contains(t, err.Error(), "version") +} + +// joinArgs is a tiny helper that lets the GPG-setopt assertions match on +// whole-argv tokens (so a substring like "signed.gpgkey=" can't accidentally +// match an unrelated dnf flag). +func joinArgs(args []string) string { + out := "" + + for i, arg := range args { + if i > 0 { + out += " " + } + + out += arg + } + + return out +} diff --git a/internal/app/azldev/cmds/repo/repo.go b/internal/app/azldev/cmds/repo/repo.go new file mode 100644 index 00000000..08378763 --- /dev/null +++ b/internal/app/azldev/cmds/repo/repo.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package repo implements the `azldev repo` top-level command, which exposes +// thin wrappers over the system dnf that auto-discover RPM repos +// under one or more URL prefixes. +package repo + +import ( + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev" + "github.com/spf13/cobra" +) + +// OnAppInit registers the `repo` command tree with app. +func OnAppInit(app *azldev.App) { + cmd := &cobra.Command{ + Use: "repo", + Short: "Inspect and manage RPM repositories", + Long: `Inspect and manage RPM repositories. + +Subcommands operate over upstream RPM repos described by an +rpm-repo-set-template (e.g. the built-in "azl-standard" layout) expanded +under one or more URL prefixes.`, + } + + app.AddTopLevelCommand(cmd) + queryOnAppInit(app, cmd) +} diff --git a/internal/app/azldev/cmds/repo/repo_test.go b/internal/app/azldev/cmds/repo/repo_test.go new file mode 100644 index 00000000..4a301767 --- /dev/null +++ b/internal/app/azldev/cmds/repo/repo_test.go @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package repo_test + +import ( + "testing" + + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev" + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/repo" + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" + "github.com/microsoft/azure-linux-dev-tools/internal/repo/repolayout" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOnAppInit(t *testing.T) { + t.Parallel() + + app := azldev.NewApp(azldev.DefaultFileSystemFactory(), azldev.DefaultOSEnvFactory()) + + require.NotPanics(t, func() { + repo.OnAppInit(app) + }) +} + +func TestNewQueryCmd_FlagsRegistered(t *testing.T) { + t.Parallel() + + cmd := repo.NewQueryCmd() + for _, name := range []string{ + "repo-prefix", "template", "arch", "no-debuginfo", "no-srpms", + "version", "use-case", + } { + assert.NotNil(t, cmd.Flags().Lookup(name), "expected flag --%s", name) + } +} + +func TestNewQueryCmd_OneOfRepoPrefixOrVersionRequired(t *testing.T) { + t.Parallel() + + cmd := repo.NewQueryCmd() + cmd.SetArgs([]string{}) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "repo-prefix") + assert.Contains(t, err.Error(), "version") +} + +func TestBuildDNFArgv_SinglePrefix(t *testing.T) { + t.Parallel() + + // Indirect coverage via running RunQuery is impractical (it execs dnf), + // but we can exercise the pure helpers by building the same input shape + // here and comparing against the expected argv structure. + templates := map[string]projectconfig.RpmRepoSetTemplate{ + "t1": { + Subrepos: []projectconfig.SubrepoSpec{ + {Name: "base", Subpath: "base/$basearch", Kind: projectconfig.SubrepoKindBinary}, + }, + }, + } + + tmpl, err := repolayout.ResolveTemplate(templates, "t1") + require.NoError(t, err) + + repos := repolayout.ExpandTemplate("https://example.com/p", "t1", tmpl, []string{"x86_64"}) + require.Len(t, repos, 1) +} diff --git a/internal/projectconfig/distro.go b/internal/projectconfig/distro.go index 677517e1..90d046f8 100644 --- a/internal/projectconfig/distro.go +++ b/internal/projectconfig/distro.go @@ -86,12 +86,20 @@ type DistroVersionDefinition struct { MockConfigPathX86_64 string `toml:"mock-config-x86_64,omitempty" json:"mockConfigX8664,omitempty" validate:"omitempty,filepath" jsonschema:"title=Mock config file,description=Path to the x86_64 mock config file for this version"` MockConfigPathAarch64 string `toml:"mock-config-aarch64,omitempty" json:"mockConfigAarch64,omitempty" validate:"omitempty,filepath" jsonschema:"title=Mock config file,description=Path to the aarch64 mock config file for this version"` - // Inputs maps build use-cases ("rpm-build", "image-build") to ordered lists - // of input references. Each entry references either a [RpmRepoResource] or - // a [RpmRepoSet]; sets are expanded at validation time. + // Inputs maps build use-cases ([UseCaseRPMBuild], [UseCaseImageBuild]) to + // ordered lists of input references. Each entry references either a + // [RpmRepoResource] or a [RpmRepoSet]; sets are expanded at validation time. Inputs DistroVersionInputs `toml:"inputs,omitempty" json:"inputs,omitempty" jsonschema:"title=Inputs,description=Per-use-case input repositories"` } +// Use-case identifiers for [DistroVersionInputs]. These match the TOML keys +// under `[distros..versions..inputs]` and are the canonical names used +// in error messages and CLI flags (e.g. `azldev repo query --use-case`). +const ( + UseCaseRPMBuild = "rpm-build" + UseCaseImageBuild = "image-build" +) + // DistroVersionInputs maps build use-cases to ordered lists of input references. // Each [DistroVersionInput] entry references either a [RpmRepoResource] (by // `repo`) or a [RpmRepoSet] (by `set`); sets are expanded at validation time diff --git a/internal/projectconfig/resources.go b/internal/projectconfig/resources.go index a306ca2b..07349c03 100644 --- a/internal/projectconfig/resources.go +++ b/internal/projectconfig/resources.go @@ -1012,14 +1012,14 @@ func validateRpmRepoSetTemplate(name string, tmpl *RpmRepoSetTemplate) error { // [ResourcesConfig.EffectiveRpmRepos] happens in [ProjectConfig.Validate] via // validateDistroVersionInputs. func (v DistroVersionDefinition) EffectiveRpmBuildRepos(resources *ResourcesConfig) ([]string, error) { - return effectiveInputRepos("rpm-build", v.Inputs.RpmBuild, resources) + return effectiveInputRepos(UseCaseRPMBuild, v.Inputs.RpmBuild, resources) } // EffectiveImageBuildRepos returns the deduplicated, ordered list of effective // repo names exposed to the image-build use-case for this distro version. Same // semantics as [DistroVersionDefinition.EffectiveRpmBuildRepos]. func (v DistroVersionDefinition) EffectiveImageBuildRepos(resources *ResourcesConfig) ([]string, error) { - return effectiveInputRepos("image-build", v.Inputs.ImageBuild, resources) + return effectiveInputRepos(UseCaseImageBuild, v.Inputs.ImageBuild, resources) } func effectiveInputRepos( diff --git a/internal/repo/repolayout/layout.go b/internal/repo/repolayout/layout.go new file mode 100644 index 00000000..be134e48 --- /dev/null +++ b/internal/repo/repolayout/layout.go @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Package repolayout resolves rpm-repo-set templates and expands them into a +// concrete list of repo URLs for the `azldev repo` subcommands. Callers +// supply the templates map (typically `Resources.RpmRepoSetTemplates` from a +// loaded `*projectconfig.ProjectConfig`), so user/project overrides on top +// of the embedded defaults flow through naturally. +package repolayout + +import ( + "errors" + "fmt" + "net/url" + "strings" + + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" + "github.com/samber/lo" +) + +// basearchPlaceholder is the substring expanded per arch in subrepo subpaths. +const basearchPlaceholder = "$basearch" + +// DefaultArches is the per-arch expansion used when the user does not pass +// `--arch`. +// +//nolint:gochecknoglobals // effectively a constant; Go has no const slices. +var DefaultArches = []string{"x86_64", "aarch64"} + +// InputRepo is one concrete (post-`$basearch`-expansion) upstream repo to query. +type InputRepo struct { + // TemplateName is the rpm-repo-set-template the repo was expanded from. + TemplateName string + // SubrepoName is the [projectconfig.SubrepoSpec.Name] this repo was expanded from. + SubrepoName string + // Kind mirrors [projectconfig.SubrepoSpec.Kind], defaulted. + Kind projectconfig.SubrepoKind + // Arch is the substituted `$basearch` value, or "" when the subpath has no + // `$basearch` (e.g., a single source/SRPM repo). + Arch string + // URL is the fully-resolved repo base URL. + URL string + // RepoID is the dnf repo id used to wire this slot up via --repofrompath / + // --enablerepo. Callers are expected to set it before passing the row to + // the dnf pipeline. + RepoID string + // GPGKey, when non-empty, is forwarded to dnf as --setopt=.gpgkey=... + // alongside gpgcheck=1. Only meaningful when the caller resolved this + // repo from project config; the prefix-driven path leaves it empty. + GPGKey string +} + +// ResolveTemplate looks up name in the supplied templates map (typically +// `Resources.RpmRepoSetTemplates` from a loaded project config, which already +// has the embedded defaults — `azl-standard`, `koji-dist-repo` — merged in +// along with any project/user overrides). Errors when the template is not +// defined. The returned template is a copy; callers may mutate it freely. +func ResolveTemplate( + templates map[string]projectconfig.RpmRepoSetTemplate, name string, +) (projectconfig.RpmRepoSetTemplate, error) { + if name == "" { + return projectconfig.RpmRepoSetTemplate{}, errors.New("template name must not be empty") + } + + if tmpl, ok := templates[name]; ok { + return tmpl, nil + } + + return projectconfig.RpmRepoSetTemplate{}, + fmt.Errorf("rpm-repo-set-template %#q is not defined", name) +} + +// ExpandTemplate expands a template into one [InputRepo] per sub-repo, fanning +// out per arch where the subpath contains `$basearch`. +func ExpandTemplate( + prefix, templateName string, + tmpl projectconfig.RpmRepoSetTemplate, + arches []string, +) []InputRepo { + out := make([]InputRepo, 0, len(tmpl.Subrepos)*len(arches)) + + for _, sub := range tmpl.Subrepos { + kind := sub.Kind.Default() + + if strings.Contains(sub.Subpath, basearchPlaceholder) { + for _, arch := range arches { + joined, _ := url.JoinPath(prefix, strings.ReplaceAll(sub.Subpath, basearchPlaceholder, arch)) + out = append(out, InputRepo{ + TemplateName: templateName, + SubrepoName: sub.Name, + Kind: kind, + Arch: arch, + URL: joined, + }) + } + + continue + } + + joined, _ := url.JoinPath(prefix, sub.Subpath) + out = append(out, InputRepo{ + TemplateName: templateName, + SubrepoName: sub.Name, + Kind: kind, + URL: joined, + }) + } + + return out +} + +// DedupInputRepos drops duplicate entries by URL while preserving order. +func DedupInputRepos(repos []InputRepo) []InputRepo { + return lo.UniqBy(repos, func(r InputRepo) string { return r.URL }) +} + +// NormalizePrefix validates prefix as an http://, https://, or file:// URL. +// Bare paths are rejected. The returned string is the parsed URL re-serialized, +// so callers downstream can safely pass it to [url.JoinPath]. +func NormalizePrefix(prefix string) (string, error) { + if prefix == "" { + return "", errors.New("empty prefix") + } + + parsed, err := url.Parse(prefix) + if err != nil { + return "", fmt.Errorf("prefix %#q is not a valid URL: %w", prefix, err) + } + + switch parsed.Scheme { + case "http", "https", "file": + default: + return "", fmt.Errorf("prefix %#q must be an http://, https://, or file:// URL", prefix) + } + + return parsed.String(), nil +} + +// SubstituteBasearch replaces every `$basearch` occurrence in raw with arch. +// Used by the version-mode resolver to bake the host arch into a URL whose +// shape carries `$basearch` literally (dnf would substitute on its own, but +// our probe layer needs a concrete URL). +func SubstituteBasearch(raw, arch string) string { + return strings.ReplaceAll(raw, basearchPlaceholder, arch) +} diff --git a/internal/repo/repolayout/layout_test.go b/internal/repo/repolayout/layout_test.go new file mode 100644 index 00000000..2390845d --- /dev/null +++ b/internal/repo/repolayout/layout_test.go @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package repolayout_test + +import ( + "testing" + + "github.com/microsoft/azure-linux-dev-tools/defaultconfigs" + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" + "github.com/microsoft/azure-linux-dev-tools/internal/repo/repolayout" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func sampleTemplates() map[string]projectconfig.RpmRepoSetTemplate { + return map[string]projectconfig.RpmRepoSetTemplate{ + defaultconfigs.DefaultRpmRepoSetTemplateName: { + Subrepos: []projectconfig.SubrepoSpec{ + {Name: "base", Subpath: "base/$basearch", Kind: projectconfig.SubrepoKindBinary}, + {Name: "base-debug", Subpath: "base/debuginfo/$basearch", Kind: projectconfig.SubrepoKindDebug}, + {Name: "base-src", Subpath: "base/srpms", Kind: projectconfig.SubrepoKindSource}, + {Name: "sdk", Subpath: "sdk/$basearch", Kind: projectconfig.SubrepoKindBinary}, + {Name: "sdk-debug", Subpath: "sdk/debuginfo/$basearch", Kind: projectconfig.SubrepoKindDebug}, + {Name: "sdk-src", Subpath: "sdk/srpms", Kind: projectconfig.SubrepoKindSource}, + }, + }, + } +} + +func TestResolveTemplate_Found(t *testing.T) { + t.Parallel() + + tmpl, err := repolayout.ResolveTemplate(sampleTemplates(), defaultconfigs.DefaultRpmRepoSetTemplateName) + require.NoError(t, err) + assert.Len(t, tmpl.Subrepos, 6) +} + +func TestResolveTemplate_NotFound(t *testing.T) { + t.Parallel() + + _, err := repolayout.ResolveTemplate(sampleTemplates(), "no-such-template") + require.Error(t, err) + assert.Contains(t, err.Error(), "not defined") +} + +func TestResolveTemplate_EmptyName(t *testing.T) { + t.Parallel() + + _, err := repolayout.ResolveTemplate(sampleTemplates(), "") + require.Error(t, err) +} + +func TestExpandTemplate(t *testing.T) { + t.Parallel() + + tmpl, err := repolayout.ResolveTemplate(sampleTemplates(), defaultconfigs.DefaultRpmRepoSetTemplateName) + require.NoError(t, err) + + repos := repolayout.ExpandTemplate( + "https://example.com/prefix/", + defaultconfigs.DefaultRpmRepoSetTemplateName, + tmpl, + []string{"x86_64", "aarch64"}, + ) + + // 2 channels x (2 per-arch binary + 2 per-arch debug + 1 source) = 10. + require.Len(t, repos, 10) + + for _, repo := range repos { + assert.NotContains(t, repo.URL, "$basearch", "$basearch must be expanded") + assert.Equal(t, defaultconfigs.DefaultRpmRepoSetTemplateName, repo.TemplateName) + } + + // Spot-check the base/binary x86_64 row. + var foundBase bool + + for _, repo := range repos { + if repo.URL == "https://example.com/prefix/base/x86_64" { + foundBase = true + + assert.Equal(t, "base", repo.SubrepoName) + assert.Equal(t, projectconfig.SubrepoKindBinary, repo.Kind) + assert.Equal(t, "x86_64", repo.Arch) + } + } + + assert.True(t, foundBase, "expected base/x86_64 row") + + // Source subrepo has empty Arch (no $basearch in subpath). + var foundSource bool + + for _, repo := range repos { + if repo.SubrepoName == "base-src" { + foundSource = true + + assert.Empty(t, repo.Arch) + assert.Equal(t, projectconfig.SubrepoKindSource, repo.Kind) + } + } + + assert.True(t, foundSource, "expected base-src row") +} + +func TestDedupInputRepos(t *testing.T) { + t.Parallel() + + repos := []repolayout.InputRepo{ + {SubrepoName: "base", Arch: "x86_64", URL: "https://a/x86_64"}, + {SubrepoName: "base", Arch: "x86_64", URL: "https://a/x86_64"}, + {SubrepoName: "base", Arch: "aarch64", URL: "https://a/aarch64"}, + } + + got := repolayout.DedupInputRepos(repos) + require.Len(t, got, 2) + assert.Equal(t, "https://a/x86_64", got[0].URL) + assert.Equal(t, "https://a/aarch64", got[1].URL) +} + +func TestNormalizePrefix(t *testing.T) { + t.Parallel() + + got, err := repolayout.NormalizePrefix("https://example.com/foo/") + require.NoError(t, err) + assert.Equal(t, "https://example.com/foo/", got) + + got, err = repolayout.NormalizePrefix("file:///tmp/repo/") + require.NoError(t, err) + assert.Equal(t, "file:///tmp/repo/", got) + + _, err = repolayout.NormalizePrefix("./testdata/repo") + require.Error(t, err) + assert.Contains(t, err.Error(), "http://") + + _, err = repolayout.NormalizePrefix("") + require.Error(t, err) +} diff --git a/pkg/app/azldev_cli/azldev.go b/pkg/app/azldev_cli/azldev.go index 90bd99fe..54872aa9 100644 --- a/pkg/app/azldev_cli/azldev.go +++ b/pkg/app/azldev_cli/azldev.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/image" "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/pkg" "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/project" + "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/repo" "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/version" ) @@ -41,6 +42,7 @@ func InstantiateApp() *azldev.App { image.OnAppInit(app) pkg.OnAppInit(app) project.OnAppInit(app) + repo.OnAppInit(app) version.OnAppInit(app) return app diff --git a/pkg/app/azldev_cli/azldev_test.go b/pkg/app/azldev_cli/azldev_test.go index 5a539ff7..acbcc855 100644 --- a/pkg/app/azldev_cli/azldev_test.go +++ b/pkg/app/azldev_cli/azldev_test.go @@ -29,6 +29,7 @@ func TestInstantiateApp(t *testing.T) { "image", "package", "project", + "repo", "version", }, ) diff --git a/scenario/__snapshots__/TestSnapshotsContainer_--bogus-flag_stderr_1.snap b/scenario/__snapshots__/TestSnapshotsContainer_--bogus-flag_stderr_1.snap index ed7793b0..827a71ea 100755 --- a/scenario/__snapshots__/TestSnapshotsContainer_--bogus-flag_stderr_1.snap +++ b/scenario/__snapshots__/TestSnapshotsContainer_--bogus-flag_stderr_1.snap @@ -9,6 +9,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell diff --git a/scenario/__snapshots__/TestSnapshotsContainer_--help_stdout_1.snap b/scenario/__snapshots__/TestSnapshotsContainer_--help_stdout_1.snap index 05278ee9..77ca08f7 100755 --- a/scenario/__snapshots__/TestSnapshotsContainer_--help_stdout_1.snap +++ b/scenario/__snapshots__/TestSnapshotsContainer_--help_stdout_1.snap @@ -16,6 +16,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell diff --git a/scenario/__snapshots__/TestSnapshotsContainer_help_stdout_1.snap b/scenario/__snapshots__/TestSnapshotsContainer_help_stdout_1.snap index 05278ee9..77ca08f7 100755 --- a/scenario/__snapshots__/TestSnapshotsContainer_help_stdout_1.snap +++ b/scenario/__snapshots__/TestSnapshotsContainer_help_stdout_1.snap @@ -16,6 +16,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell diff --git a/scenario/__snapshots__/TestSnapshots_--bogus-flag_stderr_1.snap b/scenario/__snapshots__/TestSnapshots_--bogus-flag_stderr_1.snap index ed7793b0..827a71ea 100755 --- a/scenario/__snapshots__/TestSnapshots_--bogus-flag_stderr_1.snap +++ b/scenario/__snapshots__/TestSnapshots_--bogus-flag_stderr_1.snap @@ -9,6 +9,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell diff --git a/scenario/__snapshots__/TestSnapshots_--help_stdout_1.snap b/scenario/__snapshots__/TestSnapshots_--help_stdout_1.snap index 05278ee9..77ca08f7 100755 --- a/scenario/__snapshots__/TestSnapshots_--help_stdout_1.snap +++ b/scenario/__snapshots__/TestSnapshots_--help_stdout_1.snap @@ -16,6 +16,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell diff --git a/scenario/__snapshots__/TestSnapshots_--help_with_color_stdout_1.snap b/scenario/__snapshots__/TestSnapshots_--help_with_color_stdout_1.snap index df307d9e..9711e319 100755 --- a/scenario/__snapshots__/TestSnapshots_--help_with_color_stdout_1.snap +++ b/scenario/__snapshots__/TestSnapshots_--help_with_color_stdout_1.snap @@ -16,6 +16,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell diff --git a/scenario/__snapshots__/TestSnapshots_help_stdout_1.snap b/scenario/__snapshots__/TestSnapshots_help_stdout_1.snap index 05278ee9..77ca08f7 100755 --- a/scenario/__snapshots__/TestSnapshots_help_stdout_1.snap +++ b/scenario/__snapshots__/TestSnapshots_help_stdout_1.snap @@ -16,6 +16,7 @@ Primary commands: image Manage Azure Linux images package Manage binary package configuration project Manage Azure Linux projects + repo Inspect and manage RPM repositories Meta commands: completion Generate the autocompletion script for the specified shell