diff --git a/cmd/readconfiguration.go b/cmd/readconfiguration.go index de5d22a85..533191c5e 100644 --- a/cmd/readconfiguration.go +++ b/cmd/readconfiguration.go @@ -21,6 +21,7 @@ type ReadConfigurationCmd struct { WorkspaceFolder string Config string ContainerID string + IDLabels []string IncludeFeaturesConfiguration bool IncludeMergedConfiguration bool } @@ -55,6 +56,13 @@ func NewReadConfigurationCmd(f *flags.GlobalFlags) *cobra.Command { "", "Path to a specific devcontainer.json", ) + readConfigCmd.Flags(). + StringArrayVar( + &cmd.IDLabels, + "id-label", + nil, + "Override the default container identification labels (format: key=value, can be specified multiple times)", + ) readConfigCmd.Flags(). BoolVar( &cmd.IncludeFeaturesConfiguration, @@ -89,6 +97,10 @@ func (cmd *ReadConfigurationCmd) Run( c *cobra.Command, _ []string, ) error { + if err := config2.ValidateIDLabels(cmd.IDLabels); err != nil { + return err + } + parsedConfig, workspaceFolder, err := cmd.resolve(c.Context()) if err != nil { return err @@ -129,14 +141,17 @@ func (cmd *ReadConfigurationCmd) resolve(ctx context.Context) ( string, error, ) { - if cmd.ContainerID == "" && cmd.WorkspaceFolder == "" { + if cmd.ContainerID == "" && cmd.WorkspaceFolder == "" && len(cmd.IDLabels) == 0 { return nil, "", fmt.Errorf( - "either --workspace-folder or --container-id must be provided", + "either --workspace-folder, --container-id, or --id-label must be provided", ) } if cmd.ContainerID != "" { return cmd.resolveConfigFromContainer(ctx) } + if len(cmd.IDLabels) > 0 { + return cmd.resolveConfigFromIDLabels(ctx) + } return cmd.resolveConfig() } @@ -229,6 +244,43 @@ func (cmd *ReadConfigurationCmd) resolveConfigFromContainer(ctx context.Context) return parsedConfig, workspaceFolder, nil } +func (cmd *ReadConfigurationCmd) resolveConfigFromIDLabels(ctx context.Context) ( + *config2.DevContainerConfig, + string, + error, +) { + containerDetails, err := findRunningContainer( + ctx, defaultDockerCommand, "", cmd.IDLabels, + ) + if err != nil { + return nil, "", err + } + + subCtx := &config2.SubstitutionContext{} + imageMetadata, err := metadata.GetImageMetadataFromContainer( + containerDetails, + subCtx, + ) + if err != nil { + return nil, "", fmt.Errorf("get image metadata from container: %w", err) + } + + parsedConfig := &config2.DevContainerConfig{} + if len(imageMetadata.Config) > 0 { + last := imageMetadata.Config[len(imageMetadata.Config)-1] + parsedConfig.DevContainerConfigBase = last.DevContainerConfigBase + parsedConfig.DevContainerActions = last.DevContainerActions + parsedConfig.NonComposeBase = last.NonComposeBase + } + + workspaceFolder := containerDetails.Config.WorkingDir + if workspaceFolder == "" { + workspaceFolder = "/" + } + + return parsedConfig, workspaceFolder, nil +} + func buildMergedConfig( parsedConfig *config2.DevContainerConfig, ) (*config2.MergedDevContainerConfig, error) { diff --git a/e2e/tests/readconfiguration/readconfiguration.go b/e2e/tests/readconfiguration/readconfiguration.go index ee4ac2ce1..be5d720c9 100644 --- a/e2e/tests/readconfiguration/readconfiguration.go +++ b/e2e/tests/readconfiguration/readconfiguration.go @@ -306,6 +306,41 @@ var _ = ginkgo.Describe("read-configuration command", ginkgo.Label("read-configu gomega.Expect(hr["storage"]).To(gomega.Equal("32gb")) }, ginkgo.SpecTimeout(framework.TimeoutShort())) + ginkgo.It("should read configuration using custom id-label", + func(ctx context.Context) { + tempDir, err := framework.CopyToTempDirWithoutChdir( + "tests/readconfiguration/testdata", + ) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func() { _ = os.RemoveAll(tempDir) }) + + f, err := framework.SetupDockerProvider(initialDir+"/bin", "docker") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, tempDir) + + err = f.DevsyUp(ctx, tempDir, + "--id-label", "devsy.readconfig.test=custom") + framework.ExpectNoError(err) + + stdout, _, err := f.ExecCommandCapture(ctx, []string{ + "read-configuration", + "--id-label", "devsy.readconfig.test=custom", + "--include-merged-configuration", + }) + framework.ExpectNoError(err) + + var result map[string]any + err = json.Unmarshal([]byte(stdout), &result) + framework.ExpectNoError(err, "output should be valid JSON") + + gomega.Expect(result).To(gomega.HaveKey("configuration")) + gomega.Expect(result).To(gomega.HaveKey("mergedConfiguration")) + + ws, ok := result["workspace"].(map[string]any) + gomega.Expect(ok).To(gomega.BeTrue(), "workspace should be an object") + gomega.Expect(ws).To(gomega.HaveKey("workspaceFolder")) + }, ginkgo.SpecTimeout(framework.TimeoutShort())) + ginkgo.It("reads configuration from a running container via --container-id", func(ctx context.Context) { f := framework.NewDefaultFramework(initialDir + "/bin")