diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a7bfd1f..ff3bfd7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,3 +30,74 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + docker: + runs-on: ubuntu-latest + + if: github.repository_owner == 'ctrlplanedev' + + permissions: + contents: read + id-token: write + + strategy: + matrix: + platform: [linux/amd64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Check if Docker Hub secrets are available + run: | + if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then + echo "DOCKERHUB_LOGIN=false" >> $GITHUB_ENV + else + echo "DOCKERHUB_LOGIN=true" >> $GITHUB_ENV + fi + + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: env.DOCKERHUB_LOGIN == 'true' + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ctrlplane/cli + tags: | + type=raw,value=latest + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build Only + uses: docker/build-push-action@v6 + if: env.DOCKERHUB_LOGIN != 'true' + with: + push: false + file: docker/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + CLI_VERSION=${{ steps.meta.outputs.version }} + + - name: Build and Push + uses: docker/build-push-action@v6 + if: env.DOCKERHUB_LOGIN == 'true' + with: + push: true + file: docker/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + CLI_VERSION=${{ steps.meta.outputs.version }} diff --git a/cmd/ctrlc/root/root.go b/cmd/ctrlc/root/root.go index 963e952..5f8b2f5 100644 --- a/cmd/ctrlc/root/root.go +++ b/cmd/ctrlc/root/root.go @@ -7,6 +7,7 @@ import ( "github.com/ctrlplanedev/cli/cmd/ctrlc/root/agent" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/api" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/config" + "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync" "github.com/spf13/cobra" ) @@ -44,6 +45,7 @@ func NewRootCmd() *cobra.Command { cmd.AddCommand(agent.NewAgentCmd()) cmd.AddCommand(api.NewAPICmd()) cmd.AddCommand(config.NewConfigCmd()) + cmd.AddCommand(sync.NewSyncCmd()) return cmd } diff --git a/cmd/ctrlc/root/sync/sync.go b/cmd/ctrlc/root/sync/sync.go index 58a71eb..88e69f5 100644 --- a/cmd/ctrlc/root/sync/sync.go +++ b/cmd/ctrlc/root/sync/sync.go @@ -2,18 +2,22 @@ package sync import ( "github.com/MakeNowJust/heredoc/v2" + "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/terraform" "github.com/spf13/cobra" ) -func NewRootCmd() *cobra.Command { +func NewSyncCmd() *cobra.Command { cmd := &cobra.Command{ Use: "sync ", Short: "Sync resources into Ctrlplane", Example: heredoc.Doc(` $ ctrlc sync aws-eks $ ctrlc sync google-gke + $ ctrlc sync terraform `), } + cmd.AddCommand(terraform.NewSyncTerraformCmd()) + return cmd } diff --git a/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go new file mode 100644 index 0000000..dca1900 --- /dev/null +++ b/cmd/ctrlc/root/sync/terraform/terraform-org-workspaces.go @@ -0,0 +1,185 @@ +package terraform + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "time" + + "strconv" + + "github.com/avast/retry-go" + "github.com/charmbracelet/log" + "github.com/hashicorp/go-tfe" +) + +const ( + Kind = "Workspace" + Version = "terraform/v1" +) + +type WorkspaceResource struct { + Config map[string]interface{} + Identifier string + Kind string + Metadata map[string]string + Name string + Version string +} + +func getLinksMetadata(workspace *tfe.Workspace, baseURL url.URL) *string { + if workspace.Organization == nil { + return nil + } + links := map[string]string{ + "Terraform Workspace": fmt.Sprintf("%s/app/%s/workspaces/%s", baseURL.String(), workspace.Organization.Name, workspace.Name), + } + linksJSON, err := json.Marshal(links) + if err != nil { + log.Error("Failed to marshal links", "error", err) + return nil + } + linksString := string(linksJSON) + return &linksString +} + +func getWorkspaceVariables(workspace *tfe.Workspace) map[string]string { + variables := make(map[string]string) + for _, variable := range workspace.Variables { + if variable != nil && variable.Category == tfe.CategoryTerraform && !variable.Sensitive { + key := fmt.Sprintf("terraform-cloud/variables/%s", variable.Key) + variables[key] = variable.Value + } + } + return variables +} + +func getWorkspaceVcsRepo(workspace *tfe.Workspace) map[string]string { + vcsRepo := make(map[string]string) + if workspace.VCSRepo != nil { + vcsRepo["terraform-cloud/vcs-repo/identifier"] = workspace.VCSRepo.Identifier + vcsRepo["terraform-cloud/vcs-repo/branch"] = workspace.VCSRepo.Branch + vcsRepo["terraform-cloud/vcs-repo/repository-http-url"] = workspace.VCSRepo.RepositoryHTTPURL + } + return vcsRepo +} + +func getWorkspaceTags(workspace *tfe.Workspace) map[string]string { + tags := make(map[string]string) + for _, tag := range workspace.Tags { + if tag != nil { + key := fmt.Sprintf("terraform-cloud/tag/%s", tag.Name) + tags[key] = "true" + } + } + return tags +} + +func convertWorkspaceToResource(workspace *tfe.Workspace, baseURL url.URL) (WorkspaceResource, error) { + if workspace == nil { + return WorkspaceResource{}, fmt.Errorf("workspace is nil") + } + version := Version + kind := Kind + name := workspace.Name + identifier := workspace.ID + config := map[string]interface{}{ + "workspaceId": workspace.ID, + } + metadata := map[string]string{ + "ctrlplane/external-id": workspace.ID, + "terraform-cloud/workspace-name": workspace.Name, + "terraform-cloud/workspace-auto-apply": strconv.FormatBool(workspace.AutoApply), + "terraform/version": workspace.TerraformVersion, + } + + if workspace.Organization != nil { + metadata["terraform-cloud/organization"] = workspace.Organization.Name + } + + linksMetadata := getLinksMetadata(workspace, baseURL) + if linksMetadata != nil { + metadata["ctrlplane/links"] = *linksMetadata + } + + moreValues := []map[string]string{ + getWorkspaceVariables(workspace), + getWorkspaceTags(workspace), + getWorkspaceVcsRepo(workspace), + } + + for _, moreValue := range moreValues { + for key, value := range moreValue { + metadata[key] = value + } + } + + return WorkspaceResource{ + Version: version, + Kind: kind, + Name: name, + Identifier: identifier, + Config: config, + Metadata: metadata, + }, nil +} + +func listWorkspacesWithRetry(ctx context.Context, client *tfe.Client, organization string, pageNum, pageSize int) (*tfe.WorkspaceList, error) { + var workspaces *tfe.WorkspaceList + err := retry.Do( + func() error { + var err error + workspaces, err = client.Workspaces.List(ctx, organization, &tfe.WorkspaceListOptions{ + ListOptions: tfe.ListOptions{ + PageNumber: pageNum, + PageSize: pageSize, + }, + }) + return err + }, + retry.Attempts(5), + retry.Delay(time.Second), + retry.MaxDelay(5*time.Second), + ) + return workspaces, err +} + +func listAllWorkspaces(ctx context.Context, client *tfe.Client, organization string) ([]*tfe.Workspace, error) { + var allWorkspaces []*tfe.Workspace + pageNum := 1 + pageSize := 100 + + for { + workspaces, err := listWorkspacesWithRetry(ctx, client, organization, pageNum, pageSize) + if err != nil { + return nil, fmt.Errorf("failed to list workspaces: %w", err) + } + + allWorkspaces = append(allWorkspaces, workspaces.Items...) + if len(workspaces.Items) < pageSize { + break + } + pageNum++ + } + + return allWorkspaces, nil +} + +func getWorkspacesInOrg(ctx context.Context, client *tfe.Client, organization string) ([]WorkspaceResource, error) { + workspaces, err := listAllWorkspaces(ctx, client, organization) + if err != nil { + return nil, err + } + + workspaceResources := []WorkspaceResource{} + for _, workspace := range workspaces { + workspaceResource, err := convertWorkspaceToResource(workspace, client.BaseURL()) + if err != nil { + log.Error("Failed to convert workspace to resource", "error", err, "workspace", workspace.Name) + continue + } + workspaceResources = append(workspaceResources, workspaceResource) + } + return workspaceResources, nil +} diff --git a/cmd/ctrlc/root/sync/terraform/terraform.go b/cmd/ctrlc/root/sync/terraform/terraform.go new file mode 100644 index 0000000..127fc0d --- /dev/null +++ b/cmd/ctrlc/root/sync/terraform/terraform.go @@ -0,0 +1,116 @@ +package terraform + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/ctrlplanedev/cli/internal/api" + "github.com/ctrlplanedev/cli/internal/cliutil" + "github.com/google/uuid" + "github.com/hashicorp/go-tfe" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewSyncTerraformCmd() *cobra.Command { + var workspaceId string + var organization string + + cmd := &cobra.Command{ + Use: "terraform", + Short: "Sync Terraform resources into Ctrlplane", + Example: heredoc.Doc(` + # To set the Terraform token, add TFE_TOKEN to your environment variables. + export TFE_TOKEN=... + + # To set the Terraform address, add TFE_ADDRESS to your environment variables. + export TFE_ADDRESS=... else the default address (https://app.terraform.io) is used. + + # Sync all workspaces in an organization + $ ctrlc sync terraform --organization my-org --workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 + `), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Syncing Terraform resources into Ctrlplane") + apiURL := viper.GetString("url") + apiKey := viper.GetString("api-key") + ctx := cmd.Context() + + if organization == "" { + return fmt.Errorf("organization is required") + } + + if _, err := uuid.Parse(workspaceId); err != nil { + return fmt.Errorf("invalid workspace ID: %w", err) + } + + ctrlplaneClient, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) + if err != nil { + return fmt.Errorf("failed to create API client: %w", err) + } + + terraformClient, err := tfe.NewClient(tfe.DefaultConfig()) + if err != nil { + return fmt.Errorf("failed to create Terraform client: %w", err) + } + + providerName := fmt.Sprintf("tf-%s", organization) + resp, err := ctrlplaneClient.UpsertResourceProviderWithResponse(ctx, workspaceId, providerName) + if err != nil { + return fmt.Errorf("failed to upsert resource provider: %w", err) + } + + if resp.JSON200 == nil { + return fmt.Errorf("failed to upsert resource provider: %s", resp.Body) + } + + providerId := resp.JSON200.Id + fmt.Println("Provider ID:", providerId) + workspaces, err := getWorkspacesInOrg(cmd.Context(), terraformClient, organization) + if err != nil { + return fmt.Errorf("failed to get workspaces in organization: %w", err) + } + + resources := []struct { + Config map[string]interface{} `json:"config"` + Identifier string `json:"identifier"` + Kind string `json:"kind"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Version string `json:"version"` + }{} + + for _, workspace := range workspaces { + resource := struct { + Config map[string]interface{} `json:"config"` + Identifier string `json:"identifier"` + Kind string `json:"kind"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Version string `json:"version"` + }{ + Version: workspace.Version, + Identifier: workspace.Identifier, + Metadata: workspace.Metadata, + Name: workspace.Name, + Kind: workspace.Kind, + Config: workspace.Config, + } + resources = append(resources, resource) + } + + upsertResp, err := ctrlplaneClient.SetResourceProvidersResources(ctx, providerId, api.SetResourceProvidersResourcesJSONRequestBody{ + Resources: resources, + }) + if err != nil { + return fmt.Errorf("failed to upsert resources: %w", err) + } + + return cliutil.HandleOutput(cmd, upsertResp) + }, + } + + cmd.Flags().StringVarP(&organization, "organization", "o", "", "Terraform organization name") + cmd.Flags().StringVarP(&workspaceId, "workspace-id", "w", "", "Ctrlplane workspace ID") + + return cmd +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..6b6e25c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM alpine:3.19 AS builder + +ARG CLI_VERSION + +RUN apk add --no-cache curl tar && \ + curl -L --fail "https://github.com/ctrlplanedev/cli/releases/download/${CLI_VERSION}/ctrlc_Linux_x86_64.tar.gz" -o ctrlc.tar.gz && \ + tar xzf ctrlc.tar.gz && \ + mv ctrlc /usr/local/bin/ && \ + chmod +x /usr/local/bin/ctrlc + +FROM alpine:3.19 AS final + +COPY --from=builder /usr/local/bin/ctrlc /usr/local/bin/ctrlc + +CMD ["ctrlc", "--help"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..1f8220f --- /dev/null +++ b/docker/README.md @@ -0,0 +1,40 @@ +# Ctrlplane CLI Docker Image + +Official Docker image for the Ctrlplane CLI. + +# Usage + +## Pull the image + +```sh +docker pull ctrlplane/cli:latest +``` + +or pull a specific version: + +```sh +docker pull ctrlplane/cli:v0.1.0 +``` + +## Run the image + +```sh +docker run ctrlplane/cli ctrlc [your-command] +``` + +### Required environment variables + +- `CTRLPLANE_API_KEY`: Your Ctrlplane API key. +- `CTRLPLANE_URL`: The URL of your Ctrlplane instance (e.g. `https://app.ctrlplane.dev`). + +### Terraform sync + +In order to sync Terraform resources into Ctrlplane, you need to set the following environment variables: + +- `TFE_TOKEN`: Your Terraform Cloud API token. +- `TFE_ADDRESS` (optional): The URL of your Terraform Cloud instance (e.g. `https://app.terraform.io`). If not set, the default address (`https://app.terraform.io`) is used. + +```sh +docker run -e TFE_TOKEN=my-token -e CTRLPLANE_API_KEY=my-api-key -e CTRLPLANE_URL=https://app.ctrlplane.dev \ + ctrlplane/cli ctrlc sync terraform --organization my-org --workspace-id 2a7c5560-75c9-4dbe-be74-04ee33bf8188 +``` diff --git a/go.mod b/go.mod index fa9e3e9..768d0c1 100644 --- a/go.mod +++ b/go.mod @@ -18,12 +18,20 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.10.0 // indirect github.com/charmbracelet/log v0.4.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-tfe v1.73.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/jsonapi v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -44,8 +52,10 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.6.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7d67ccd..971f436 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRr github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -25,14 +27,29 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-tfe v1.73.1 h1:JmS5OClA4SkW8MygjgbSovYc8W4rPCUpxqXWEUVsOII= +github.com/hashicorp/go-tfe v1.73.1/go.mod h1:1+oOnpyJ+I/shr8GV+pdB8wzitFWO9p1VOkDhUSlyj0= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= +github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -110,12 +127,19 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=