Skip to content
Snippets Groups Projects
Commit d225867a authored by Gary Holtz's avatar Gary Holtz :two:
Browse files

Merge branch 'agent-bootstrap-cmd' into 'main'

feat: cluster agent bootstrap command

See merge request !1645



Merged-by: default avatarGary Holtz <gholtz@gitlab.com>
Approved-by: default avatarJames Hebden <jhebden@gitlab.com>
Approved-by: default avatarGary Holtz <gholtz@gitlab.com>
Co-authored-by: Timo Furrer's avatarTimo Furrer <tfurrer@gitlab.com>
parents dde28689 0b1571a8
No related branches found
No related tags found
1 merge request!1645feat: cluster agent bootstrap command
Pipeline #1466006701 passed
Showing
with 2393 additions and 6 deletions
......@@ -27,8 +27,6 @@ config:
no-trailing-punctuation: # MD026
punctuation: ".,;:!。,;:!?"
no-trailing-spaces: false # MD009
ol-prefix: # MD029
style: "one"
reference-links-images: false # MD052
ul-style: # MD004
style: "dash"
......
package api
import "github.com/xanzy/go-gitlab"
import (
"errors"
"fmt"
"slices"
"time"
"github.com/xanzy/go-gitlab"
)
// agentTokenLimit specifies the maximal amount of agent tokens that can be active per agent at any given time.
const agentTokenLimit = 2
var AgentNotFoundErr = errors.New("agent not found")
var ListAgents = func(client *gitlab.Client, projectID interface{}, opts *gitlab.ListAgentsOptions) ([]*gitlab.Agent, error) {
if client == nil {
......@@ -27,3 +39,78 @@ var GetAgent = func(client *gitlab.Client, projectID interface{}, agentID int) (
return agent, nil
}
var GetAgentByName = func(client *gitlab.Client, projectID interface{}, agentName string) (*gitlab.Agent, error) {
opts := &gitlab.ListAgentsOptions{
Page: 1,
PerPage: 100,
}
for opts.Page != 0 {
paginatedAgents, resp, err := client.ClusterAgents.ListAgents(projectID, opts)
if err != nil {
return nil, err
}
for _, agent := range paginatedAgents {
if agent.Name == agentName {
// found
return agent, nil
}
}
opts.Page = resp.NextPage
}
return nil, AgentNotFoundErr
}
var RegisterAgent = func(client *gitlab.Client, projectID interface{}, agentName string) (*gitlab.Agent, error) {
if client == nil {
client = apiClient.Lab()
}
agent, _, err := client.ClusterAgents.RegisterAgent(projectID, &gitlab.RegisterAgentOptions{Name: gitlab.Ptr(agentName)})
if err != nil {
return nil, err
}
return agent, nil
}
var CreateAgentToken = func(client *gitlab.Client, projectID interface{}, agentID int, recreateOnLimit bool) (*gitlab.AgentToken, bool /* recreated */, error) {
recreated := false
if recreateOnLimit {
tokens, _, err := client.ClusterAgents.ListAgentTokens(projectID, agentID, &gitlab.ListAgentTokensOptions{PerPage: agentTokenLimit})
if err != nil {
return nil, false, err
}
if len(tokens) == agentTokenLimit {
slices.SortFunc(tokens, agentTokenSortFunc)
longestUnusedToken := tokens[0]
_, err := client.ClusterAgents.RevokeAgentToken(projectID, agentID, longestUnusedToken.ID)
if err != nil {
return nil, false, err
}
recreated = true
}
}
// create new token
token, _, err := client.ClusterAgents.CreateAgentToken(projectID, agentID, &gitlab.CreateAgentTokenOptions{
Name: gitlab.Ptr(fmt.Sprintf("glab-bootstrap-%d", time.Now().UTC().Unix())),
Description: gitlab.Ptr("Created by the `glab cluster agent bootstrap command"),
})
return token, recreated, err
}
func agentTokenSortFunc(a, b *gitlab.AgentToken) int {
if a.LastUsedAt == nil {
return 1
}
if b.LastUsedAt == nil {
return -1
}
return a.LastUsedAt.Compare(*b.LastUsedAt)
}
package api
import "github.com/xanzy/go-gitlab"
import (
"fmt"
"net/http"
"github.com/xanzy/go-gitlab"
)
const (
commitAuthorName = "glab"
commitAuthorEmail = "noreply@glab.gitlab.com"
)
// GetFile retrieves a file from repository. Note that file content is Base64 encoded.
var GetFile = func(client *gitlab.Client, projectID interface{}, path string, ref string) (*gitlab.File, error) {
......@@ -18,3 +28,35 @@ var GetFile = func(client *gitlab.Client, projectID interface{}, path string, re
return file, nil
}
// SyncFile syncs (add or update) a file in the repository
var SyncFile = func(client *gitlab.Client, projectID interface{}, path string, content []byte, ref string) error {
_, resp, err := client.RepositoryFiles.GetFileMetaData(projectID, path, &gitlab.GetFileMetaDataOptions{
Ref: gitlab.Ptr(ref),
})
if err != nil {
if resp.StatusCode != http.StatusNotFound {
return err
}
// file does not exist yet, lets create it
_, _, err := client.RepositoryFiles.CreateFile(projectID, path, &gitlab.CreateFileOptions{
Branch: gitlab.Ptr(ref),
Content: gitlab.Ptr(string(content)),
CommitMessage: gitlab.Ptr(fmt.Sprintf("Add %s via glab file sync", path)),
AuthorName: gitlab.Ptr(commitAuthorName),
AuthorEmail: gitlab.Ptr(commitAuthorEmail),
})
return err
}
// file already exists, lets update it
_, _, err = client.RepositoryFiles.UpdateFile(projectID, path, &gitlab.UpdateFileOptions{
Branch: gitlab.Ptr(ref),
Content: gitlab.Ptr(string(content)),
CommitMessage: gitlab.Ptr(fmt.Sprintf("Update %s via glab file sync", path)),
AuthorName: gitlab.Ptr(commitAuthorName),
AuthorEmail: gitlab.Ptr(commitAuthorEmail),
})
return err
}
......@@ -2,6 +2,7 @@ package cluster
import (
"github.com/spf13/cobra"
agentBootstrapCmd "gitlab.com/gitlab-org/cli/commands/cluster/agent/bootstrap"
checkManifestUsageCmd "gitlab.com/gitlab-org/cli/commands/cluster/agent/check_manifest_usage"
agentGetTokenCmd "gitlab.com/gitlab-org/cli/commands/cluster/agent/get_token"
agentListCmd "gitlab.com/gitlab-org/cli/commands/cluster/agent/list"
......@@ -23,5 +24,14 @@ func NewCmdAgent(f *cmdutils.Factory) *cobra.Command {
agentCmd.AddCommand(agentUpdateKubeconfigCmd.NewCmdAgentUpdateKubeconfig(f))
agentCmd.AddCommand(checkManifestUsageCmd.NewCmdCheckManifestUsage(f))
agentCmd.AddCommand(agentBootstrapCmd.NewCmdAgentBootstrap(
f,
agentBootstrapCmd.EnsureRequirements,
agentBootstrapCmd.NewAPI,
agentBootstrapCmd.NewLocalKubectlWrapper,
agentBootstrapCmd.NewLocalFluxWrapper,
agentBootstrapCmd.NewCmd,
))
return agentCmd
}
......@@ -5,7 +5,10 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/test"
)
......@@ -14,7 +17,13 @@ func TestNewCmdAgent(t *testing.T) {
r, w, _ := os.Pipe()
os.Stdout = w
assert.Nil(t, NewCmdAgent(&cmdutils.Factory{}).Execute())
assert.Nil(t, NewCmdAgent(&cmdutils.Factory{
IO: &iostreams.IOStreams{
StdOut: os.Stdout,
},
HttpClient: func() (*gitlab.Client, error) { return nil, nil },
BaseRepo: func() (glrepo.Interface, error) { return glrepo.New("OWNER", "REPO"), nil },
}).Execute())
out := test.ReturnBuffer(old, r, w)
......
package bootstrap
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
glab_api "gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)
//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -typed -destination=./mocks_for_test.go -package=bootstrap gitlab.com/gitlab-org/cli/commands/cluster/agent/bootstrap API,FluxWrapper,KubectlWrapper,Cmd
//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -typed -destination=./stdlib_mocks_for_test.go -package=bootstrap "io" "Writer"
type API interface {
GetDefaultBranch() (string, error)
GetAgentByName(name string) (*gitlab.Agent, error)
RegisterAgent(name string) (*gitlab.Agent, error)
CreateAgentToken(agentID int) (*gitlab.AgentToken, error)
SyncFile(f file, branch string) error
}
type FluxWrapper interface {
createHelmRepositoryManifest() (file, error)
createHelmReleaseManifest() (file, error)
reconcile() error
}
type KubectlWrapper interface {
createAgentTokenSecret(token string) error
}
type (
APIFactory func(*gitlab.Client, any) API
KubectlWrapperFactory func(Cmd, string, string, string) KubectlWrapper
FluxWrapperFactory func(Cmd, string, string, string, string, string, string, string, string, string, string, string, string) FluxWrapper
CmdFactory func(io.Writer, io.Writer, []string) Cmd
)
var reconcileErr = errors.New("failed to reconcile the GitLab Agent")
const (
kubectlBinaryName = "kubectl"
fluxBinaryName = "flux"
)
func EnsureRequirements() error {
if _, err := exec.LookPath(kubectlBinaryName); err != nil {
return fmt.Errorf("unable to find %s binary in PATH", kubectlBinaryName)
}
if _, err := exec.LookPath(fluxBinaryName); err != nil {
return fmt.Errorf("unable to find %s binary in PATH", fluxBinaryName)
}
return nil
}
func NewCmdAgentBootstrap(f *cmdutils.Factory, ensureRequirements func() error, af APIFactory, kwf KubectlWrapperFactory, fwf FluxWrapperFactory, cf CmdFactory) *cobra.Command {
agentBootstrapCmd := &cobra.Command{
Use: "bootstrap agent-name [flags]",
Short: `Bootstrap a GitLab Agent for Kubernetes in a project.`,
Long: `Bootstrap a GitLab Agent for Kubernetes (agentk) in a project.
The first argument must be the name of the agent.
It requires the kubectl and flux commands to be accessible via $PATH.
This command consists of multiple idempotent steps:
1. Register the agent with the project.
2. Create a token for the agent.
- If the agent has reached the maximum amount of tokens,
the one that has not been used the longest is revoked
and a new one is created.
- If the agent has not reached the maximum amount of tokens,
a new one is created.
3. Push the Kubernetes Secret that contains the token to the cluster.
4. Create Flux HelmRepository and HelmRelease resource.
5. Commit and Push the created Flux Helm resources to the manifest path.
6. Trigger Flux reconciliation of GitLab Agent HelmRelease.
`,
Example: `
# Bootstrap "my-agent" to root of Git project in CWD and trigger reconciliation
glab cluster agent bootstrap my-agent
# Bootstrap "my-agent" to "manifests/" of Git project in CWD and trigger reconciliation
glab cluster agent bootstrap my-agent --manifest-path manifests/
# Bootstrap "my-agent" to "manifests/" of Git project in CWD and do not manually trigger a reconilication
glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
`,
Aliases: []string{"bs"},
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return ensureRequirements()
},
RunE: func(cmd *cobra.Command, args []string) error {
stdout, stderr := f.IO.StdOut, f.IO.StdErr
apiClient, err := f.HttpClient()
if err != nil {
return err
}
repo, err := f.BaseRepo()
if err != nil {
return err
}
api := af(apiClient, repo.FullName())
manifestPath, err := cmd.Flags().GetString("manifest-path")
if err != nil {
return err
}
manifestBranch, err := cmd.Flags().GetString("manifest-branch")
if err != nil {
return err
}
if manifestBranch == "" {
manifestBranch, err = api.GetDefaultBranch()
if err != nil {
return err
}
}
noReconcile, err := cmd.Flags().GetBool("no-reconcile")
if err != nil {
return err
}
helmRepositoryName, err := cmd.Flags().GetString("helm-repository-name")
if err != nil {
return err
}
helmRepositoryNamespace, err := cmd.Flags().GetString("helm-repository-namespace")
if err != nil {
return err
}
helmRepositoryFilepath, err := cmd.Flags().GetString("helm-repository-filepath")
if err != nil {
return err
}
helmReleaseName, err := cmd.Flags().GetString("helm-release-name")
if err != nil {
return err
}
helmReleaseNamespace, err := cmd.Flags().GetString("helm-release-namespace")
if err != nil {
return err
}
helmReleaseFilepath, err := cmd.Flags().GetString("helm-release-filepath")
if err != nil {
return err
}
helmReleaseTargetNamespace, err := cmd.Flags().GetString("helm-release-target-namespace")
if err != nil {
return err
}
gitlabAgentTokenSecretName, err := cmd.Flags().GetString("gitlab-agent-token-secret-name")
if err != nil {
return err
}
fluxSourceType, err := cmd.Flags().GetString("flux-source-type")
if err != nil {
return err
}
fluxSourceNamespace, err := cmd.Flags().GetString("flux-source-namespace")
if err != nil {
return err
}
fluxSourceName, err := cmd.Flags().GetString("flux-source-name")
if err != nil {
return err
}
c := cf(stdout, stderr, os.Environ())
return (&bootstrapCmd{
api: api,
stdout: stdout,
stderr: stderr,
agentName: args[0],
manifestBranch: manifestBranch,
kubectl: kwf(c, kubectlBinaryName, helmReleaseTargetNamespace, gitlabAgentTokenSecretName),
flux: fwf(
c, fluxBinaryName, manifestPath,
helmRepositoryName, helmRepositoryNamespace, helmRepositoryFilepath,
helmReleaseName, helmReleaseNamespace, helmReleaseFilepath, helmReleaseTargetNamespace,
fluxSourceType, fluxSourceNamespace, fluxSourceName,
),
noReconcile: noReconcile,
}).run()
},
}
agentBootstrapCmd.Flags().StringP("manifest-path", "p", "", "Location of directory in Git repository for storing the GitLab Agent for Kubernetes Helm resources.")
agentBootstrapCmd.Flags().StringP("manifest-branch", "b", "", "Branch to commit the Flux Manifests to. (default to the project default branch)")
agentBootstrapCmd.Flags().Bool("no-reconcile", false, "Do not trigger Flux reconciliation for GitLab Agent for Kubernetes Flux resource.")
agentBootstrapCmd.Flags().String("helm-repository-name", "gitlab", "Name of the Flux HelmRepository manifest.")
agentBootstrapCmd.Flags().String("helm-repository-namespace", "flux-system", "Namespace of the Flux HelmRepository manifest.")
agentBootstrapCmd.Flags().String("helm-repository-filepath", "gitlab-helm-repository.yaml", "Filepath within the GitLab Agent project to commit the Flux HelmRepository to.")
agentBootstrapCmd.Flags().String("helm-release-name", "gitlab-agent", "Name of the Flux HelmRelease manifest.")
agentBootstrapCmd.Flags().String("helm-release-namespace", "flux-system", "Namespace of the Flux HelmRelease manifest.")
agentBootstrapCmd.Flags().String("helm-release-filepath", "gitlab-agent-helm-release.yaml", "Filepath within the GitLab Agent project to commit the Flux HelmRelease to.")
agentBootstrapCmd.Flags().String("helm-release-target-namespace", "gitlab-agent", "Namespace of the GitLab Agent deployment.")
agentBootstrapCmd.Flags().String("gitlab-agent-token-secret-name", "gitlab-agent-token", "Name of the Secret where the token for the GitLab Agent is stored. The helm-release-target-namespace is implied for the namespace of the Secret.")
agentBootstrapCmd.Flags().String("flux-source-type", "git", "Source type of the flux-system, e.g. git, oci, helm, ...")
agentBootstrapCmd.Flags().String("flux-source-namespace", "flux-system", "Flux source namespace.")
agentBootstrapCmd.Flags().String("flux-source-name", "flux-system", "Flux source name.")
return agentBootstrapCmd
}
type bootstrapCmd struct {
api API
stdout io.Writer
stderr io.Writer
agentName string
manifestBranch string
kubectl KubectlWrapper
flux FluxWrapper
noReconcile bool
}
type file struct {
path string
content []byte
}
func (c *bootstrapCmd) run() error {
// 1. Register the agent
fmt.Fprintf(c.stderr, "Registering Agent ... ")
agent, err := c.registerAgent()
if err != nil {
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 2. Create a token for the registered agent
fmt.Fprintf(c.stderr, "Creating Agent Token ... ")
token, err := c.createAgentToken(agent)
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 3. Push token in Kubernetes secret to cluster
fmt.Fprintf(c.stderr, "Creating Kubernetes Secret with Agent Token ... ")
err = c.createAgentTokenKubernetesSecret(token)
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 4. Create Flux HelmRepository and HelmRelease resource.
fmt.Fprintf(c.stderr, "Creating Flux Helm Resources ... ")
helmResourceFiles, err := c.createFluxHelmResources()
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 5. Commit and Push the created Flux Helm resources to the manifest path.
fmt.Fprintf(c.stderr, "Syncing Flux Helm Resources ... ")
err = c.syncFluxHelmResourceFiles(helmResourceFiles)
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
if !c.noReconcile {
// 6. Trigger Flux reconciliation of GitLab Agent HelmRelease.
fmt.Fprintln(c.stderr, "Reconciling Flux Helm Resources ... Output from flux command:")
err = c.fluxReconcile()
if err != nil {
return reconcileErr
}
}
fmt.Fprintln(c.stderr, "Successfully bootstrapped the GitLab Agent")
return nil
}
func (c *bootstrapCmd) registerAgent() (*gitlab.Agent, error) {
agent, err := c.api.GetAgentByName(c.agentName)
if err != nil {
if !errors.Is(err, glab_api.AgentNotFoundErr) {
return nil, err
}
// register agent
agent, err = c.api.RegisterAgent(c.agentName)
if err != nil {
return nil, err
}
}
return agent, nil
}
func (c *bootstrapCmd) createAgentToken(agent *gitlab.Agent) (*gitlab.AgentToken, error) {
return c.api.CreateAgentToken(agent.ID)
}
func (c *bootstrapCmd) createAgentTokenKubernetesSecret(token *gitlab.AgentToken) error {
return c.kubectl.createAgentTokenSecret(token.Token)
}
func (c *bootstrapCmd) createFluxHelmResources() ([]file, error) {
helmRepository, err := c.flux.createHelmRepositoryManifest()
if err != nil {
return nil, err
}
helmRelease, err := c.flux.createHelmReleaseManifest()
if err != nil {
return nil, err
}
return []file{helmRepository, helmRelease}, nil
}
func (c *bootstrapCmd) syncFluxHelmResourceFiles(files []file) error {
for _, f := range files {
err := c.api.SyncFile(f, c.manifestBranch)
if err != nil {
return err
}
}
return nil
}
func (c *bootstrapCmd) fluxReconcile() error {
return c.flux.reconcile()
}
This diff is collapsed.
package bootstrap
import (
"github.com/xanzy/go-gitlab"
glab_api "gitlab.com/gitlab-org/cli/api"
)
var _ API = (*apiWrapper)(nil)
func NewAPI(client *gitlab.Client, projectID any) API {
return &apiWrapper{client: client, projectID: projectID}
}
type apiWrapper struct {
client *gitlab.Client
projectID any
}
func (a *apiWrapper) GetDefaultBranch() (string, error) {
project, err := glab_api.GetProject(a.client, a.projectID)
if err != nil {
return "", err
}
return project.DefaultBranch, nil
}
func (a *apiWrapper) GetAgentByName(name string) (*gitlab.Agent, error) {
return glab_api.GetAgentByName(a.client, a.projectID, name)
}
func (a *apiWrapper) RegisterAgent(name string) (*gitlab.Agent, error) {
return glab_api.RegisterAgent(a.client, a.projectID, name)
}
func (a *apiWrapper) CreateAgentToken(agentID int) (*gitlab.AgentToken, error) {
token, _, err := glab_api.CreateAgentToken(a.client, a.projectID, agentID, true)
return token, err
}
func (a *apiWrapper) SyncFile(f file, branch string) error {
return glab_api.SyncFile(a.client, a.projectID, f.path, f.content, branch)
}
package bootstrap
import (
"fmt"
"io"
"os/exec"
)
type Cmd interface {
RunWithOutput(name string, arg ...string) ([]byte, error)
Run(name string, arg ...string) error
}
type cmdWrapper struct {
stdout, stderr io.Writer
env []string
}
type errorWithOutput struct {
output []byte
err error
}
func (e errorWithOutput) Error() string {
return fmt.Sprintf("command failed with %q and output:\n%s", e.err, e.output)
}
func (e errorWithOutput) Unwrap() error {
return e.err
}
func NewCmd(stdout, stderr io.Writer, env []string) Cmd {
return &cmdWrapper{
stdout: stdout,
stderr: stderr,
env: env,
}
}
func (c *cmdWrapper) RunWithOutput(name string, arg ...string) ([]byte, error) {
command := exec.Command(name, arg...)
command.Env = c.env
output, err := command.CombinedOutput()
if err != nil {
return output, &errorWithOutput{output: output, err: err}
}
return output, nil
}
func (c *cmdWrapper) Run(name string, arg ...string) error {
command := exec.Command(name, arg...)
command.Stdout = c.stdout
command.Stderr = c.stderr
command.Env = c.env
return command.Run()
}
package bootstrap
import (
"bytes"
"errors"
"fmt"
"os"
"path"
"time"
"github.com/avast/retry-go/v4"
)
var _ FluxWrapper = (*localFluxWrapper)(nil)
func NewLocalFluxWrapper(
cmd Cmd,
binary string,
manifestPath string,
helmRepositoryName string,
helmRepositoryNamespace string,
helmRepositoryFilepath string,
helmReleaseName string,
helmReleaseNamespace string,
helmReleaseFilepath string,
helmReleaseTargetNamespace string,
fluxSourceType string,
fluxSourceNamespace string,
fluxSourceName string,
) FluxWrapper {
return &localFluxWrapper{
cmd: cmd,
binary: binary,
manifestPath: manifestPath,
helmRepositoryName: helmRepositoryName,
helmRepositoryNamespace: helmRepositoryNamespace,
helmRepositoryFilepath: helmRepositoryFilepath,
helmReleaseName: helmReleaseName,
helmReleaseNamespace: helmReleaseNamespace,
helmReleaseFilepath: helmReleaseFilepath,
helmReleaseTargetNamespace: helmReleaseTargetNamespace,
fluxSourceType: fluxSourceType,
fluxSourceNamespace: fluxSourceNamespace,
fluxSourceName: fluxSourceName,
reconcileRetryDelay: 10 * time.Second,
}
}
type localFluxWrapper struct {
cmd Cmd
binary string
manifestPath string
helmRepositoryName string
helmRepositoryNamespace string
helmRepositoryFilepath string
helmReleaseName string
helmReleaseNamespace string
helmReleaseFilepath string
helmReleaseTargetNamespace string
fluxSourceType string
fluxSourceNamespace string
fluxSourceName string
reconcileRetryDelay time.Duration
}
func (f *localFluxWrapper) createHelmRepositoryManifest() (file, error) {
helmRepositoryYAML, err := f.cmd.RunWithOutput(
f.binary,
"create",
"source",
"helm",
f.helmRepositoryName,
"--export",
fmt.Sprintf("-n=%s", f.helmRepositoryNamespace),
"--url=https://charts.gitlab.io",
)
if err != nil {
return file{}, err
}
return file{path: path.Join(f.manifestPath, f.helmRepositoryFilepath), content: helmRepositoryYAML}, nil
}
func (f *localFluxWrapper) createHelmReleaseManifest() (file, error) {
// create temporary file for Flux CLI to read values from.
// The Flux CLI does not yet support reading values from literal flags.
valuesFile, err := os.CreateTemp("", "glab-bootstrap-helmrelease-values")
if err != nil {
return file{}, err
}
defer os.Remove(valuesFile.Name())
defer valuesFile.Close()
_, err = valuesFile.Write([]byte(`config:
secretName: gitlab-agent-token
`))
if err != nil {
return file{}, err
}
if err = valuesFile.Sync(); err != nil {
return file{}, err
}
helmReleaseYAML, err := f.cmd.RunWithOutput(
f.binary,
"create",
"helmrelease",
f.helmReleaseName,
"--export",
fmt.Sprintf("-n=%s", f.helmReleaseNamespace),
fmt.Sprintf("--target-namespace=%s", f.helmReleaseTargetNamespace),
"--create-target-namespace=true",
fmt.Sprintf("--source=HelmRepository/%s.%s", f.helmRepositoryName, f.helmRepositoryNamespace),
"--chart=gitlab-agent",
fmt.Sprintf("--release-name=%s", f.helmReleaseName),
fmt.Sprintf("--values=%s", valuesFile.Name()),
)
if err != nil {
return file{}, err
}
return file{path: path.Join(f.manifestPath, f.helmReleaseFilepath), content: helmReleaseYAML}, nil
}
func (f *localFluxWrapper) reconcile() error {
// reconcile flux source to pull new HelmRepository source
err := f.cmd.Run(f.binary, "reconcile", "source", f.fluxSourceType, f.fluxSourceName, fmt.Sprintf("-n=%s", f.fluxSourceNamespace))
if err != nil {
return err
}
// just reconciling doesn't mean that the HelmRelease now exists ... (bug in flux? At least very unfortunate behavior)
err = retry.Do(func() error {
output, err := f.cmd.RunWithOutput(f.binary, "get", "helmreleases", f.helmReleaseName, fmt.Sprintf("-n=%s", f.helmReleaseNamespace))
if err != nil {
// flux always returns with exit code 0, even when the helmrelease does not exist (yet)
return retry.Unrecoverable(err)
}
if bytes.Contains(output, []byte(fmt.Sprintf(`HelmRelease object '%s' not found in "%s" namespace`, f.helmReleaseName, f.helmReleaseNamespace))) {
return errors.New(string(output))
}
return nil
}, retry.Attempts(6), retry.Delay(f.reconcileRetryDelay))
if err != nil {
return err
}
return f.cmd.Run(f.binary, "reconcile", "helmrelease", f.helmReleaseName, fmt.Sprintf("-n=%s", f.helmReleaseNamespace), "--with-source")
}
package bootstrap
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestFlux_createHelmRepositoryManifest(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
mockCmd.EXPECT().RunWithOutput(
"flux", "create", "source", "helm", "helm-repository-name", "--export",
"-n=helm-repository-namespace", "--url=https://charts.gitlab.io").
Return([]byte("content"), nil)
actualFile, err := f.createHelmRepositoryManifest()
// THEN
require.NoError(t, err)
assert.Equal(t, actualFile.path, "manifest-path/helm-repository-filepath")
assert.Equal(t, actualFile.content, []byte("content"))
}
func TestFlux_createHelmRepositoryManifest_Failure(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
mockCmd.EXPECT().RunWithOutput(
"flux", "create", "source", "helm", "helm-repository-name", "--export",
"-n=helm-repository-namespace", "--url=https://charts.gitlab.io").
Return(nil, errors.New("test"))
actualFile, err := f.createHelmRepositoryManifest()
// THEN
require.Error(t, err)
assert.Equal(t, actualFile, file{})
}
func TestFlux_createHelmReReleaseManifest(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
mockCmd.EXPECT().RunWithOutput(
"flux", "create", "helmrelease", "helm-release-name", "--export",
"-n=helm-release-namespace", "--target-namespace=helm-release-target-namespace",
"--create-target-namespace=true", "--source=HelmRepository/helm-repository-name.helm-repository-namespace",
"--chart=gitlab-agent", "--release-name=helm-release-name", StartsWith("--values=")).
Return([]byte("content"), nil)
actualFile, err := f.createHelmReleaseManifest()
// THEN
require.NoError(t, err)
assert.Equal(t, actualFile.path, "manifest-path/helm-release-filepath")
assert.Equal(t, actualFile.content, []byte("content"))
}
func TestFlux_createHelmReReleaseManifest_Failure(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
mockCmd.EXPECT().RunWithOutput(
"flux", "create", "helmrelease", "helm-release-name", "--export",
"-n=helm-release-namespace", "--target-namespace=helm-release-target-namespace",
"--create-target-namespace=true", "--source=HelmRepository/helm-repository-name.helm-repository-namespace",
"--chart=gitlab-agent", "--release-name=helm-release-name", StartsWith("--values=")).
Return([]byte(""), errors.New("test"))
actualFile, err := f.createHelmReleaseManifest()
// THEN
require.Error(t, err)
assert.Equal(t, actualFile, file{})
}
func TestFlux_reconcile(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
gomock.InOrder(
mockCmd.EXPECT().Run("flux", "reconcile", "source", "flux-source-type", "flux-source-name", "-n=flux-source-namespace"),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace"),
mockCmd.EXPECT().Run("flux", "reconcile", "helmrelease", "helm-release-name", "-n=helm-release-namespace", "--with-source"),
)
// WHEN
_ = f.reconcile()
}
func TestFlux_reconcile_retries(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
gomock.InOrder(
mockCmd.EXPECT().Run("flux", "reconcile", "source", "flux-source-type", "flux-source-name", "-n=flux-source-namespace"),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace"),
mockCmd.EXPECT().Run("flux", "reconcile", "helmrelease", "helm-release-name", "-n=helm-release-namespace", "--with-source"),
)
// WHEN
_ = f.reconcile()
}
func TestFlux_reconcile_abort_retry_max(t *testing.T) {
// GIVEN
mockCmd, f := setupFlux(t)
gomock.InOrder(
mockCmd.EXPECT().Run("flux", "reconcile", "source", "flux-source-type", "flux-source-name", "-n=flux-source-namespace"),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
mockCmd.EXPECT().RunWithOutput("flux", "get", "helmreleases", "helm-release-name", "-n=helm-release-namespace").Return([]byte(`HelmRelease object 'helm-release-name' not found in "helm-release-namespace" namespace`), nil),
)
// WHEN
err := f.reconcile()
assert.Error(t, err)
}
func setupFlux(t *testing.T) (*MockCmd, FluxWrapper) {
ctrl := gomock.NewController(t)
mockCmd := NewMockCmd(ctrl)
f := NewLocalFluxWrapper(
mockCmd,
"flux", "manifest-path",
"helm-repository-name", "helm-repository-namespace", "helm-repository-filepath",
"helm-release-name", "helm-release-namespace", "helm-release-filepath", "helm-release-target-namespace",
"flux-source-type", "flux-source-namespace", "flux-source-name",
)
fHack := f.(*localFluxWrapper)
fHack.reconcileRetryDelay = 0
return mockCmd, f
}
func StartsWith(prefix string) gomock.Matcher {
return &startsWithMatcher{prefix: prefix}
}
type startsWithMatcher struct {
prefix string
actualS string
}
func (m startsWithMatcher) Matches(arg interface{}) bool {
m.actualS = arg.(string)
return strings.HasPrefix(m.actualS, m.prefix)
}
func (m startsWithMatcher) String() string {
return fmt.Sprintf("does not start with: %q, got %q", m.prefix, m.actualS)
}
package bootstrap
import (
"bytes"
"fmt"
)
var _ KubectlWrapper = (*localKubectlWrapper)(nil)
func NewLocalKubectlWrapper(cmd Cmd, binary string, gitlabAgentNamespace string, gitlabAgentTokenSecretName string) KubectlWrapper {
return &localKubectlWrapper{
cmd: cmd,
binary: binary,
gitlabAgentNamespace: gitlabAgentNamespace,
gitlabAgentTokenSecretName: gitlabAgentTokenSecretName,
}
}
type localKubectlWrapper struct {
cmd Cmd
binary string
gitlabAgentNamespace string
gitlabAgentTokenSecretName string
}
func (k *localKubectlWrapper) createAgentTokenSecret(token string) error {
namespaceFlag := fmt.Sprintf("-n=%s", k.gitlabAgentNamespace)
output, err := k.cmd.RunWithOutput(k.binary, "create", "namespace", k.gitlabAgentNamespace)
if err != nil {
if !bytes.Contains(output, []byte("already exists")) {
return err
}
// let's not even bother to first check if the secret exists or not - just attempt to delete it ...
output, err = k.cmd.RunWithOutput(k.binary, "delete", "secret", k.gitlabAgentTokenSecretName, namespaceFlag)
if err != nil {
if !bytes.Contains(output, []byte("not found")) {
return err
}
}
}
// create the secret again with the next token
_, err = k.cmd.RunWithOutput(k.binary, "create", "secret", "generic", k.gitlabAgentTokenSecretName, namespaceFlag, "--type=Opaque", fmt.Sprintf("--from-literal=token=%s", token))
if err != nil {
return err
}
return nil
}
package bootstrap
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func TestKubectl_createAgentSecretToken_NewNamespace(t *testing.T) {
// GIVEN
mockCmd, k := setupKubectl(t)
gomock.InOrder(
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "namespace", "gitlab-agent"),
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "secret", "generic", "gitlab-agent-token", "-n=gitlab-agent", "--type=Opaque", "--from-literal=token=any-token"),
)
// WHEN
err := k.createAgentTokenSecret("any-token")
// THEN
assert.NoError(t, err)
}
func TestKubectl_createAgentSecretToken_NamespaceAlreadyExists(t *testing.T) {
// GIVEN
mockCmd, k := setupKubectl(t)
gomock.InOrder(
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "namespace", "gitlab-agent").Return([]byte("already exists"), errors.New("test")),
mockCmd.EXPECT().RunWithOutput("kubectl", "delete", "secret", "gitlab-agent-token", "-n=gitlab-agent").Return([]byte("not found"), errors.New("test")),
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "secret", "generic", "gitlab-agent-token", "-n=gitlab-agent", "--type=Opaque", "--from-literal=token=any-token"),
)
// WHEN
err := k.createAgentTokenSecret("any-token")
// THEN
assert.NoError(t, err)
}
func TestKubectl_createAgentSecretToken_NamespaceCreationFails(t *testing.T) {
// GIVEN
mockCmd, k := setupKubectl(t)
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "namespace", "gitlab-agent").Return([]byte("unknown error"), errors.New("test"))
// WHEN
err := k.createAgentTokenSecret("any-token")
// THEN
assert.Error(t, err)
}
func TestKubectl_createAgentSecretToken_SecretDeletionFails(t *testing.T) {
// GIVEN
mockCmd, k := setupKubectl(t)
gomock.InOrder(
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "namespace", "gitlab-agent").Return([]byte("already exists"), errors.New("test")),
mockCmd.EXPECT().RunWithOutput("kubectl", "delete", "secret", "gitlab-agent-token", "-n=gitlab-agent").Return([]byte("unknown error"), errors.New("test")),
)
// WHEN
err := k.createAgentTokenSecret("any-token")
// THEN
assert.Error(t, err)
}
func TestKubectl_createAgentSecretToken_SecretCreationFails(t *testing.T) {
// GIVEN
mockCmd, k := setupKubectl(t)
gomock.InOrder(
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "namespace", "gitlab-agent"),
mockCmd.EXPECT().RunWithOutput("kubectl", "create", "secret", "generic", "gitlab-agent-token", "-n=gitlab-agent", "--type=Opaque", "--from-literal=token=any-token").Return([]byte("unknown error"), errors.New("test")),
)
// WHEN
err := k.createAgentTokenSecret("any-token")
// THEN
assert.Error(t, err)
}
func setupKubectl(t *testing.T) (*MockCmd, KubectlWrapper) {
ctrl := gomock.NewController(t)
mockCmd := NewMockCmd(ctrl)
k := NewLocalKubectlWrapper(mockCmd, "kubectl", "gitlab-agent", "gitlab-agent-token")
return mockCmd, k
}
// Code generated by MockGen. DO NOT EDIT.
// Source: gitlab.com/gitlab-org/cli/commands/cluster/agent/bootstrap (interfaces: API,FluxWrapper,KubectlWrapper,Cmd)
//
// Generated by this command:
//
// mockgen -typed -destination=./mocks_for_test.go -package=bootstrap gitlab.com/gitlab-org/cli/commands/cluster/agent/bootstrap API,FluxWrapper,KubectlWrapper,Cmd
//
// Package bootstrap is a generated GoMock package.
package bootstrap
import (
reflect "reflect"
gitlab "github.com/xanzy/go-gitlab"
gomock "go.uber.org/mock/gomock"
)
// MockAPI is a mock of API interface.
type MockAPI struct {
ctrl *gomock.Controller
recorder *MockAPIMockRecorder
}
// MockAPIMockRecorder is the mock recorder for MockAPI.
type MockAPIMockRecorder struct {
mock *MockAPI
}
// NewMockAPI creates a new mock instance.
func NewMockAPI(ctrl *gomock.Controller) *MockAPI {
mock := &MockAPI{ctrl: ctrl}
mock.recorder = &MockAPIMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
return m.recorder
}
// CreateAgentToken mocks base method.
func (m *MockAPI) CreateAgentToken(arg0 int) (*gitlab.AgentToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAgentToken", arg0)
ret0, _ := ret[0].(*gitlab.AgentToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateAgentToken indicates an expected call of CreateAgentToken.
func (mr *MockAPIMockRecorder) CreateAgentToken(arg0 any) *MockAPICreateAgentTokenCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAgentToken", reflect.TypeOf((*MockAPI)(nil).CreateAgentToken), arg0)
return &MockAPICreateAgentTokenCall{Call: call}
}
// MockAPICreateAgentTokenCall wrap *gomock.Call
type MockAPICreateAgentTokenCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPICreateAgentTokenCall) Return(arg0 *gitlab.AgentToken, arg1 error) *MockAPICreateAgentTokenCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPICreateAgentTokenCall) Do(f func(int) (*gitlab.AgentToken, error)) *MockAPICreateAgentTokenCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPICreateAgentTokenCall) DoAndReturn(f func(int) (*gitlab.AgentToken, error)) *MockAPICreateAgentTokenCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetAgentByName mocks base method.
func (m *MockAPI) GetAgentByName(arg0 string) (*gitlab.Agent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAgentByName", arg0)
ret0, _ := ret[0].(*gitlab.Agent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAgentByName indicates an expected call of GetAgentByName.
func (mr *MockAPIMockRecorder) GetAgentByName(arg0 any) *MockAPIGetAgentByNameCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentByName", reflect.TypeOf((*MockAPI)(nil).GetAgentByName), arg0)
return &MockAPIGetAgentByNameCall{Call: call}
}
// MockAPIGetAgentByNameCall wrap *gomock.Call
type MockAPIGetAgentByNameCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPIGetAgentByNameCall) Return(arg0 *gitlab.Agent, arg1 error) *MockAPIGetAgentByNameCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPIGetAgentByNameCall) Do(f func(string) (*gitlab.Agent, error)) *MockAPIGetAgentByNameCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPIGetAgentByNameCall) DoAndReturn(f func(string) (*gitlab.Agent, error)) *MockAPIGetAgentByNameCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// GetDefaultBranch mocks base method.
func (m *MockAPI) GetDefaultBranch() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDefaultBranch")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDefaultBranch indicates an expected call of GetDefaultBranch.
func (mr *MockAPIMockRecorder) GetDefaultBranch() *MockAPIGetDefaultBranchCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultBranch", reflect.TypeOf((*MockAPI)(nil).GetDefaultBranch))
return &MockAPIGetDefaultBranchCall{Call: call}
}
// MockAPIGetDefaultBranchCall wrap *gomock.Call
type MockAPIGetDefaultBranchCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPIGetDefaultBranchCall) Return(arg0 string, arg1 error) *MockAPIGetDefaultBranchCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPIGetDefaultBranchCall) Do(f func() (string, error)) *MockAPIGetDefaultBranchCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPIGetDefaultBranchCall) DoAndReturn(f func() (string, error)) *MockAPIGetDefaultBranchCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// RegisterAgent mocks base method.
func (m *MockAPI) RegisterAgent(arg0 string) (*gitlab.Agent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegisterAgent", arg0)
ret0, _ := ret[0].(*gitlab.Agent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RegisterAgent indicates an expected call of RegisterAgent.
func (mr *MockAPIMockRecorder) RegisterAgent(arg0 any) *MockAPIRegisterAgentCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterAgent", reflect.TypeOf((*MockAPI)(nil).RegisterAgent), arg0)
return &MockAPIRegisterAgentCall{Call: call}
}
// MockAPIRegisterAgentCall wrap *gomock.Call
type MockAPIRegisterAgentCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPIRegisterAgentCall) Return(arg0 *gitlab.Agent, arg1 error) *MockAPIRegisterAgentCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPIRegisterAgentCall) Do(f func(string) (*gitlab.Agent, error)) *MockAPIRegisterAgentCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPIRegisterAgentCall) DoAndReturn(f func(string) (*gitlab.Agent, error)) *MockAPIRegisterAgentCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SyncFile mocks base method.
func (m *MockAPI) SyncFile(arg0 file, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SyncFile", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SyncFile indicates an expected call of SyncFile.
func (mr *MockAPIMockRecorder) SyncFile(arg0, arg1 any) *MockAPISyncFileCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncFile", reflect.TypeOf((*MockAPI)(nil).SyncFile), arg0, arg1)
return &MockAPISyncFileCall{Call: call}
}
// MockAPISyncFileCall wrap *gomock.Call
type MockAPISyncFileCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPISyncFileCall) Return(arg0 error) *MockAPISyncFileCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPISyncFileCall) Do(f func(file, string) error) *MockAPISyncFileCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPISyncFileCall) DoAndReturn(f func(file, string) error) *MockAPISyncFileCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockFluxWrapper is a mock of FluxWrapper interface.
type MockFluxWrapper struct {
ctrl *gomock.Controller
recorder *MockFluxWrapperMockRecorder
}
// MockFluxWrapperMockRecorder is the mock recorder for MockFluxWrapper.
type MockFluxWrapperMockRecorder struct {
mock *MockFluxWrapper
}
// NewMockFluxWrapper creates a new mock instance.
func NewMockFluxWrapper(ctrl *gomock.Controller) *MockFluxWrapper {
mock := &MockFluxWrapper{ctrl: ctrl}
mock.recorder = &MockFluxWrapperMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFluxWrapper) EXPECT() *MockFluxWrapperMockRecorder {
return m.recorder
}
// createHelmReleaseManifest mocks base method.
func (m *MockFluxWrapper) createHelmReleaseManifest() (file, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "createHelmReleaseManifest")
ret0, _ := ret[0].(file)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// createHelmReleaseManifest indicates an expected call of createHelmReleaseManifest.
func (mr *MockFluxWrapperMockRecorder) createHelmReleaseManifest() *MockFluxWrappercreateHelmReleaseManifestCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createHelmReleaseManifest", reflect.TypeOf((*MockFluxWrapper)(nil).createHelmReleaseManifest))
return &MockFluxWrappercreateHelmReleaseManifestCall{Call: call}
}
// MockFluxWrappercreateHelmReleaseManifestCall wrap *gomock.Call
type MockFluxWrappercreateHelmReleaseManifestCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockFluxWrappercreateHelmReleaseManifestCall) Return(arg0 file, arg1 error) *MockFluxWrappercreateHelmReleaseManifestCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockFluxWrappercreateHelmReleaseManifestCall) Do(f func() (file, error)) *MockFluxWrappercreateHelmReleaseManifestCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockFluxWrappercreateHelmReleaseManifestCall) DoAndReturn(f func() (file, error)) *MockFluxWrappercreateHelmReleaseManifestCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// createHelmRepositoryManifest mocks base method.
func (m *MockFluxWrapper) createHelmRepositoryManifest() (file, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "createHelmRepositoryManifest")
ret0, _ := ret[0].(file)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// createHelmRepositoryManifest indicates an expected call of createHelmRepositoryManifest.
func (mr *MockFluxWrapperMockRecorder) createHelmRepositoryManifest() *MockFluxWrappercreateHelmRepositoryManifestCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createHelmRepositoryManifest", reflect.TypeOf((*MockFluxWrapper)(nil).createHelmRepositoryManifest))
return &MockFluxWrappercreateHelmRepositoryManifestCall{Call: call}
}
// MockFluxWrappercreateHelmRepositoryManifestCall wrap *gomock.Call
type MockFluxWrappercreateHelmRepositoryManifestCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockFluxWrappercreateHelmRepositoryManifestCall) Return(arg0 file, arg1 error) *MockFluxWrappercreateHelmRepositoryManifestCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockFluxWrappercreateHelmRepositoryManifestCall) Do(f func() (file, error)) *MockFluxWrappercreateHelmRepositoryManifestCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockFluxWrappercreateHelmRepositoryManifestCall) DoAndReturn(f func() (file, error)) *MockFluxWrappercreateHelmRepositoryManifestCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// reconcile mocks base method.
func (m *MockFluxWrapper) reconcile() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "reconcile")
ret0, _ := ret[0].(error)
return ret0
}
// reconcile indicates an expected call of reconcile.
func (mr *MockFluxWrapperMockRecorder) reconcile() *MockFluxWrapperreconcileCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "reconcile", reflect.TypeOf((*MockFluxWrapper)(nil).reconcile))
return &MockFluxWrapperreconcileCall{Call: call}
}
// MockFluxWrapperreconcileCall wrap *gomock.Call
type MockFluxWrapperreconcileCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockFluxWrapperreconcileCall) Return(arg0 error) *MockFluxWrapperreconcileCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockFluxWrapperreconcileCall) Do(f func() error) *MockFluxWrapperreconcileCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockFluxWrapperreconcileCall) DoAndReturn(f func() error) *MockFluxWrapperreconcileCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockKubectlWrapper is a mock of KubectlWrapper interface.
type MockKubectlWrapper struct {
ctrl *gomock.Controller
recorder *MockKubectlWrapperMockRecorder
}
// MockKubectlWrapperMockRecorder is the mock recorder for MockKubectlWrapper.
type MockKubectlWrapperMockRecorder struct {
mock *MockKubectlWrapper
}
// NewMockKubectlWrapper creates a new mock instance.
func NewMockKubectlWrapper(ctrl *gomock.Controller) *MockKubectlWrapper {
mock := &MockKubectlWrapper{ctrl: ctrl}
mock.recorder = &MockKubectlWrapperMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockKubectlWrapper) EXPECT() *MockKubectlWrapperMockRecorder {
return m.recorder
}
// createAgentTokenSecret mocks base method.
func (m *MockKubectlWrapper) createAgentTokenSecret(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "createAgentTokenSecret", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// createAgentTokenSecret indicates an expected call of createAgentTokenSecret.
func (mr *MockKubectlWrapperMockRecorder) createAgentTokenSecret(arg0 any) *MockKubectlWrappercreateAgentTokenSecretCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createAgentTokenSecret", reflect.TypeOf((*MockKubectlWrapper)(nil).createAgentTokenSecret), arg0)
return &MockKubectlWrappercreateAgentTokenSecretCall{Call: call}
}
// MockKubectlWrappercreateAgentTokenSecretCall wrap *gomock.Call
type MockKubectlWrappercreateAgentTokenSecretCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockKubectlWrappercreateAgentTokenSecretCall) Return(arg0 error) *MockKubectlWrappercreateAgentTokenSecretCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockKubectlWrappercreateAgentTokenSecretCall) Do(f func(string) error) *MockKubectlWrappercreateAgentTokenSecretCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockKubectlWrappercreateAgentTokenSecretCall) DoAndReturn(f func(string) error) *MockKubectlWrappercreateAgentTokenSecretCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockCmd is a mock of Cmd interface.
type MockCmd struct {
ctrl *gomock.Controller
recorder *MockCmdMockRecorder
}
// MockCmdMockRecorder is the mock recorder for MockCmd.
type MockCmdMockRecorder struct {
mock *MockCmd
}
// NewMockCmd creates a new mock instance.
func NewMockCmd(ctrl *gomock.Controller) *MockCmd {
mock := &MockCmd{ctrl: ctrl}
mock.recorder = &MockCmdMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCmd) EXPECT() *MockCmdMockRecorder {
return m.recorder
}
// Run mocks base method.
func (m *MockCmd) Run(arg0 string, arg1 ...string) error {
m.ctrl.T.Helper()
varargs := []any{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Run", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Run indicates an expected call of Run.
func (mr *MockCmdMockRecorder) Run(arg0 any, arg1 ...any) *MockCmdRunCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0}, arg1...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockCmd)(nil).Run), varargs...)
return &MockCmdRunCall{Call: call}
}
// MockCmdRunCall wrap *gomock.Call
type MockCmdRunCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockCmdRunCall) Return(arg0 error) *MockCmdRunCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockCmdRunCall) Do(f func(string, ...string) error) *MockCmdRunCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockCmdRunCall) DoAndReturn(f func(string, ...string) error) *MockCmdRunCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// RunWithOutput mocks base method.
func (m *MockCmd) RunWithOutput(arg0 string, arg1 ...string) ([]byte, error) {
m.ctrl.T.Helper()
varargs := []any{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "RunWithOutput", varargs...)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunWithOutput indicates an expected call of RunWithOutput.
func (mr *MockCmdMockRecorder) RunWithOutput(arg0 any, arg1 ...any) *MockCmdRunWithOutputCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0}, arg1...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunWithOutput", reflect.TypeOf((*MockCmd)(nil).RunWithOutput), varargs...)
return &MockCmdRunWithOutputCall{Call: call}
}
// MockCmdRunWithOutputCall wrap *gomock.Call
type MockCmdRunWithOutputCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockCmdRunWithOutputCall) Return(arg0 []byte, arg1 error) *MockCmdRunWithOutputCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockCmdRunWithOutputCall) Do(f func(string, ...string) ([]byte, error)) *MockCmdRunWithOutputCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockCmdRunWithOutputCall) DoAndReturn(f func(string, ...string) ([]byte, error)) *MockCmdRunWithOutputCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Code generated by MockGen. DO NOT EDIT.
// Source: io (interfaces: Writer)
//
// Generated by this command:
//
// mockgen -typed -destination=./stdlib_mocks_for_test.go -package=bootstrap io Writer
//
// Package bootstrap is a generated GoMock package.
package bootstrap
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockWriter is a mock of Writer interface.
type MockWriter struct {
ctrl *gomock.Controller
recorder *MockWriterMockRecorder
}
// MockWriterMockRecorder is the mock recorder for MockWriter.
type MockWriterMockRecorder struct {
mock *MockWriter
}
// NewMockWriter creates a new mock instance.
func NewMockWriter(ctrl *gomock.Controller) *MockWriter {
mock := &MockWriter{ctrl: ctrl}
mock.recorder = &MockWriterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
return m.recorder
}
// Write mocks base method.
func (m *MockWriter) Write(arg0 []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Write indicates an expected call of Write.
func (mr *MockWriterMockRecorder) Write(arg0 any) *MockWriterWriteCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockWriter)(nil).Write), arg0)
return &MockWriterWriteCall{Call: call}
}
// MockWriterWriteCall wrap *gomock.Call
type MockWriterWriteCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockWriterWriteCall) Return(arg0 int, arg1 error) *MockWriterWriteCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockWriterWriteCall) Do(f func([]byte) (int, error)) *MockWriterWriteCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockWriterWriteCall) DoAndReturn(f func([]byte) (int, error)) *MockWriterWriteCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
......@@ -5,7 +5,10 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/test"
)
......@@ -14,7 +17,13 @@ func TestNewCmdCluster(t *testing.T) {
r, w, _ := os.Pipe()
os.Stdout = w
assert.Nil(t, NewCmdCluster(&cmdutils.Factory{}).Execute())
assert.Nil(t, NewCmdCluster(&cmdutils.Factory{
IO: &iostreams.IOStreams{
StdOut: os.Stdout,
},
HttpClient: func() (*gitlab.Client, error) { return nil, nil },
BaseRepo: func() (glrepo.Interface, error) { return glrepo.New("OWNER", "REPO"), nil },
}).Execute())
out := test.ReturnBuffer(old, r, w)
......
---
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->
# `glab cluster agent bootstrap`
Bootstrap a GitLab Agent for Kubernetes in a project.
## Synopsis
Bootstrap a GitLab Agent for Kubernetes (agentk) in a project.
The first argument must be the name of the agent.
It requires the kubectl and flux commands to be accessible via $PATH.
This command consists of multiple idempotent steps:
1. Register the agent with the project.
2. Create a token for the agent.
- If the agent has reached the maximum amount of tokens,
the one that has not been used the longest is revoked
and a new one is created.
- If the agent has not reached the maximum amount of tokens,
a new one is created.
3. Push the Kubernetes Secret that contains the token to the cluster.
4. Create Flux HelmRepository and HelmRelease resource.
5. Commit and Push the created Flux Helm resources to the manifest path.
6. Trigger Flux reconciliation of GitLab Agent HelmRelease.
```plaintext
glab cluster agent bootstrap agent-name [flags]
```
## Aliases
```plaintext
bs
```
## Examples
```plaintext
# Bootstrap "my-agent" to root of Git project in CWD and trigger reconciliation
glab cluster agent bootstrap my-agent
# Bootstrap "my-agent" to "manifests/" of Git project in CWD and trigger reconciliation
glab cluster agent bootstrap my-agent --manifest-path manifests/
# Bootstrap "my-agent" to "manifests/" of Git project in CWD and do not manually trigger a reconilication
glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
```
## Options
```plaintext
--flux-source-name string Flux source name. (default "flux-system")
--flux-source-namespace string Flux source namespace. (default "flux-system")
--flux-source-type string Source type of the flux-system, e.g. git, oci, helm, ... (default "git")
--gitlab-agent-token-secret-name string Name of the Secret where the token for the GitLab Agent is stored. The helm-release-target-namespace is implied for the namespace of the Secret. (default "gitlab-agent-token")
--helm-release-filepath string Filepath within the GitLab Agent project to commit the Flux HelmRelease to. (default "gitlab-agent-helm-release.yaml")
--helm-release-name string Name of the Flux HelmRelease manifest. (default "gitlab-agent")
--helm-release-namespace string Namespace of the Flux HelmRelease manifest. (default "flux-system")
--helm-release-target-namespace string Namespace of the GitLab Agent deployment. (default "gitlab-agent")
--helm-repository-filepath string Filepath within the GitLab Agent project to commit the Flux HelmRepository to. (default "gitlab-helm-repository.yaml")
--helm-repository-name string Name of the Flux HelmRepository manifest. (default "gitlab")
--helm-repository-namespace string Namespace of the Flux HelmRepository manifest. (default "flux-system")
-b, --manifest-branch string Branch to commit the Flux Manifests to. (default to the project default branch)
-p, --manifest-path string Location of directory in Git repository for storing the GitLab Agent for Kubernetes Helm resources.
--no-reconcile Do not trigger Flux reconciliation for GitLab Agent for Kubernetes Flux resource.
```
## Options inherited from parent commands
```plaintext
--help Show help for this command.
-R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL.
```
......@@ -27,6 +27,7 @@ Manage GitLab Agents for Kubernetes.
## Subcommands
- [`bootstrap`](bootstrap.md)
- [`check_manifest_usage`](check_manifest_usage.md)
- [`get-token`](get-token.md)
- [`list`](list.md)
......
......@@ -95,10 +95,13 @@ require (
github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-emoji v1.0.3 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
......
......@@ -241,6 +241,8 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
......@@ -255,6 +257,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
......@@ -309,6 +313,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment