diff --git a/pkg/devcontainer/config.go b/pkg/devcontainer/config.go index 40ea904b2..ada9ccba5 100644 --- a/pkg/devcontainer/config.go +++ b/pkg/devcontainer/config.go @@ -168,7 +168,7 @@ func (r *runner) substitute( } substitutionContext := &config.SubstitutionContext{ - DevContainerID: r.ID, + DevContainerID: config.DeriveDevContainerID(r.LocalWorkspaceFolder), LocalWorkspaceFolder: r.LocalWorkspaceFolder, ContainerWorkspaceFolder: containerWorkspaceFolder, Env: env, diff --git a/pkg/devcontainer/config/substitute.go b/pkg/devcontainer/config/substitute.go index f6838279b..29d21e3db 100644 --- a/pkg/devcontainer/config/substitute.go +++ b/pkg/devcontainer/config/substitute.go @@ -1,6 +1,8 @@ package config import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "math/big" "path/filepath" @@ -11,6 +13,8 @@ import ( "github.com/devsy-org/devsy/pkg/hash" ) +const devContainerIDLength = 20 + type ReplaceFunction func(match, variable string, args []string) string var VariableRegExp = regexp.MustCompile(`\${(.*?)}`) @@ -226,10 +230,19 @@ func ListToObject(list []string) map[string]string { return ret } -func GetDevContainerID(labels map[string]string) string { +func DeriveDevContainerID(localWorkspaceFolder string) string { + h := sha256.Sum256([]byte(localWorkspaceFolder)) + return hex.EncodeToString(h[:])[:devContainerIDLength] +} + +func GetLegacyDevContainerID(labels map[string]string) string { labelsBytes, _ := json.Marshal(labels) hashedLabels := hash.String(string(labelsBytes)) bigInt := big.Int{} bigInt.SetString(hashedLabels, 16) return bigInt.Text(32) } + +func ResolveDevContainerID(localWorkspaceFolder string, legacyLabels map[string]string) string { + return DeriveDevContainerID(localWorkspaceFolder) +} diff --git a/pkg/devcontainer/config/substitute_test.go b/pkg/devcontainer/config/substitute_test.go index 5801516ab..7bc1ce4a5 100644 --- a/pkg/devcontainer/config/substitute_test.go +++ b/pkg/devcontainer/config/substitute_test.go @@ -1,6 +1,10 @@ package config -import "testing" +import ( + "crypto/sha256" + "encoding/hex" + "testing" +) func TestLookupValue(t *testing.T) { tests := []struct { @@ -119,3 +123,53 @@ func TestResolveStringDefaultWithColons(t *testing.T) { ) } } + +const testWorkspaceFolder = "/home/user/project" + +func TestDeriveDevContainerID(t *testing.T) { + h := sha256.Sum256([]byte(testWorkspaceFolder)) + want := hex.EncodeToString(h[:])[:devContainerIDLength] + + got := DeriveDevContainerID(testWorkspaceFolder) + if got != want { + t.Errorf("DeriveDevContainerID(%q) = %q, want %q", testWorkspaceFolder, got, want) + } + if len(got) != devContainerIDLength { + t.Errorf("expected length %d, got %d", devContainerIDLength, len(got)) + } +} + +func TestDeriveDevContainerIDDeterministic(t *testing.T) { + first := DeriveDevContainerID(testWorkspaceFolder) + second := DeriveDevContainerID(testWorkspaceFolder) + if first != second { + t.Errorf("DeriveDevContainerID is not deterministic: %q != %q", first, second) + } +} + +func TestGetLegacyDevContainerID(t *testing.T) { + labels := map[string]string{ + "dev.containers.id": "test-workspace", + } + got := GetLegacyDevContainerID(labels) + if got == "" { + t.Error("GetLegacyDevContainerID returned empty string") + } + + again := GetLegacyDevContainerID(labels) + if got != again { + t.Errorf("GetLegacyDevContainerID is not deterministic: %q != %q", got, again) + } +} + +func TestResolveDevContainerID(t *testing.T) { + labels := map[string]string{ + "dev.containers.id": "test-workspace", + } + + got := ResolveDevContainerID(testWorkspaceFolder, labels) + want := DeriveDevContainerID(testWorkspaceFolder) + if got != want { + t.Errorf("ResolveDevContainerID() = %q, want %q (spec-based ID)", got, want) + } +}