From eb70f4fb3fa3be946caffffaf2e4fd65f1f87fbe Mon Sep 17 00:00:00 2001 From: Alberto Carretero Date: Fri, 12 Jan 2024 17:06:41 +0100 Subject: [PATCH 1/2] feat: add support for chisel release format "v1" This commits adds support for "v1" which changes the "v1-public-keys" field into "public-keys". The "chisel-v1" format is still supported. --- README.md | 39 ++++++++- internal/setup/releaseChiselV1.go | 110 ++++++++++++++++++++++++ internal/setup/releaseV1.go | 110 ++++++++++++++++++++++++ internal/setup/setup.go | 133 ++++++------------------------ internal/setup/setup_test.go | 26 +++++- internal/slicer/slicer_test.go | 24 +++++- 6 files changed, 332 insertions(+), 110 deletions(-) create mode 100644 internal/setup/releaseChiselV1.go create mode 100644 internal/setup/releaseV1.go diff --git a/README.md b/README.md index 503c085e..58e4fad5 100644 --- a/README.md +++ b/README.md @@ -119,13 +119,50 @@ archives: Example: ```yaml -format: chisel-v1 +format: v1 archives: ubuntu: version: 22.04 components: [main, universe] suites: [jammy, jammy-security, jammy-updates] + public-keys: [ubuntu-archive-key-2018] + +public-keys: + # Ubuntu Archive Automatic Signing Key (2018) + # rsa4096/f6ecb3762474eda9d21b7022871920d1991bc93c 2018-09-17T15:01:46Z + ubuntu-archive-key-2018: + id: "871920D1991BC93C" + armor: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + + mQINBFufwdoBEADv/Gxytx/LcSXYuM0MwKojbBye81s0G1nEx+lz6VAUpIUZnbkq + dXBHC+dwrGS/CeeLuAjPRLU8AoxE/jjvZVp8xFGEWHYdklqXGZ/gJfP5d3fIUBtZ + HZEJl8B8m9pMHf/AQQdsC+YzizSG5t5Mhnotw044LXtdEEkx2t6Jz0OGrh+5Ioxq + X7pZiq6Cv19BohaUioKMdp7ES6RYfN7ol6HSLFlrMXtVfh/ijpN9j3ZhVGVeRC8k + KHQsJ5PkIbmvxBiUh7SJmfZUx0IQhNMaDHXfdZAGNtnhzzNReb1FqNLSVkrS/Pns + AQzMhG1BDm2VOSF64jebKXffFqM5LXRQTeqTLsjUbbrqR6s/GCO8UF7jfUj6I7ta + LygmsHO/JD4jpKRC0gbpUBfaiJyLvuepx3kWoqL3sN0LhlMI80+fA7GTvoOx4tpq + VlzlE6TajYu+jfW3QpOFS5ewEMdL26hzxsZg/geZvTbArcP+OsJKRmhv4kNo6Ayd + yHQ/3ZV/f3X9mT3/SPLbJaumkgp3Yzd6t5PeBu+ZQk/mN5WNNuaihNEV7llb1Zhv + Y0Fxu9BVd/BNl0rzuxp3rIinB2TX2SCg7wE5xXkwXuQ/2eTDE0v0HlGntkuZjGow + DZkxHZQSxZVOzdZCRVaX/WEFLpKa2AQpw5RJrQ4oZ/OfifXyJzP27o03wQARAQAB + tEJVYnVudHUgQXJjaGl2ZSBBdXRvbWF0aWMgU2lnbmluZyBLZXkgKDIwMTgpIDxm + dHBtYXN0ZXJAdWJ1bnR1LmNvbT6JAjgEEwEKACIFAlufwdoCGwMGCwkIBwMCBhUI + AgkKCwQWAgMBAh4BAheAAAoJEIcZINGZG8k8LHMQAKS2cnxz/5WaoCOWArf5g6UH + beOCgc5DBm0hCuFDZWWv427aGei3CPuLw0DGLCXZdyc5dqE8mvjMlOmmAKKlj1uG + g3TYCbQWjWPeMnBPZbkFgkZoXJ7/6CB7bWRht1sHzpt1LTZ+SYDwOwJ68QRp7DRa + Zl9Y6QiUbeuhq2DUcTofVbBxbhrckN4ZteLvm+/nG9m/ciopc66LwRdkxqfJ32Cy + q+1TS5VaIJDG7DWziG+Kbu6qCDM4QNlg3LH7p14CrRxAbc4lvohRgsV4eQqsIcdF + kuVY5HPPj2K8TqpY6STe8Gh0aprG1RV8ZKay3KSMpnyV1fAKn4fM9byiLzQAovC0 + LZ9MMMsrAS/45AvC3IEKSShjLFn1X1dRCiO6/7jmZEoZtAp53hkf8SMBsi78hVNr + BumZwfIdBA1v22+LY4xQK8q4XCoRcA9G+pvzU9YVW7cRnDZZGl0uwOw7z9PkQBF5 + KFKjWDz4fCk+K6+YtGpovGKekGBb8I7EA6UpvPgqA/QdI0t1IBP0N06RQcs1fUaA + QEtz6DGy5zkRhR4pGSZn+dFET7PdAjEK84y7BdY4t+U1jcSIvBj0F2B7LwRL7xGp + SpIKi/ekAXLs117bvFHaCvmUYN7JVp1GMmVFxhIdx6CFm3fxG8QjNb5tere/YqK+ + uOgcXny1UlwtCUzlrSaP + =9AdM + -----END PGP PUBLIC KEY BLOCK----- ``` #### Slice definitions diff --git a/internal/setup/releaseChiselV1.go b/internal/setup/releaseChiselV1.go new file mode 100644 index 00000000..d5be1aa7 --- /dev/null +++ b/internal/setup/releaseChiselV1.go @@ -0,0 +1,110 @@ +package setup + +import ( + "bytes" + "fmt" + + "golang.org/x/crypto/openpgp/packet" + "gopkg.in/yaml.v3" + + "github.com/canonical/chisel/internal/pgputil" +) + +func parseReleaseChiselV1(baseDir, filePath string, data []byte) (*Release, error) { + type yamlArchive struct { + Version string `yaml:"version"` + Suites []string `yaml:"suites"` + Components []string `yaml:"components"` + Default bool `yaml:"default"` + PubKeys []string `yaml:"v1-public-keys"` + } + type yamlPubKey struct { + ID string `yaml:"id"` + Armor string `yaml:"armor"` + } + type yamlRelease struct { + Format string `yaml:"format"` + Archives map[string]yamlArchive `yaml:"archives"` + PubKeys map[string]yamlPubKey `yaml:"v1-public-keys"` + } + const yamlReleaseFormat = "chisel-v1" + + release := &Release{ + Path: baseDir, + Packages: make(map[string]*Package), + Archives: make(map[string]*Archive), + } + + fileName := stripBase(baseDir, filePath) + + yamlVar := yamlRelease{} + dec := yaml.NewDecoder(bytes.NewBuffer(data)) + dec.KnownFields(false) + err := dec.Decode(&yamlVar) + if err != nil { + return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) + } + if yamlVar.Format != yamlReleaseFormat { + return nil, fmt.Errorf("%s: expected format %q, got %q", fileName, yamlReleaseFormat, yamlVar.Format) + } + if len(yamlVar.Archives) == 0 { + return nil, fmt.Errorf("%s: no archives defined", fileName) + } + + // Decode the public keys and match against provided IDs. + pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) + for keyName, yamlPubKey := range yamlVar.PubKeys { + key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) + if err != nil { + return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) + } + if yamlPubKey.ID != key.KeyIdString() { + return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) + } + pubKeys[keyName] = key + } + + for archiveName, details := range yamlVar.Archives { + if details.Version == "" { + return nil, fmt.Errorf("%s: archive %q missing version field", fileName, archiveName) + } + if len(details.Suites) == 0 { + adjective := ubuntuAdjectives[details.Version] + if adjective == "" { + return nil, fmt.Errorf("%s: archive %q missing suites field", fileName, archiveName) + } + details.Suites = []string{adjective} + } + if len(details.Components) == 0 { + return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) + } + if len(yamlVar.Archives) == 1 { + details.Default = true + } else if details.Default && release.DefaultArchive != "" { + return nil, fmt.Errorf("%s: more than one default archive: %s, %s", fileName, release.DefaultArchive, archiveName) + } + if details.Default { + release.DefaultArchive = archiveName + } + if len(details.PubKeys) == 0 { + return nil, fmt.Errorf("%s: archive %q missing v1-public-keys field", fileName, archiveName) + } + var archiveKeys []*packet.PublicKey + for _, keyName := range details.PubKeys { + key, ok := pubKeys[keyName] + if !ok { + return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) + } + archiveKeys = append(archiveKeys, key) + } + release.Archives[archiveName] = &Archive{ + Name: archiveName, + Version: details.Version, + Suites: details.Suites, + Components: details.Components, + PubKeys: archiveKeys, + } + } + + return release, err +} diff --git a/internal/setup/releaseV1.go b/internal/setup/releaseV1.go new file mode 100644 index 00000000..5ea46071 --- /dev/null +++ b/internal/setup/releaseV1.go @@ -0,0 +1,110 @@ +package setup + +import ( + "bytes" + "fmt" + + "golang.org/x/crypto/openpgp/packet" + "gopkg.in/yaml.v3" + + "github.com/canonical/chisel/internal/pgputil" +) + +func parseReleaseV1(baseDir, filePath string, data []byte) (*Release, error) { + type yamlArchive struct { + Version string `yaml:"version"` + Suites []string `yaml:"suites"` + Components []string `yaml:"components"` + Default bool `yaml:"default"` + PubKeys []string `yaml:"public-keys"` + } + type yamlPubKey struct { + ID string `yaml:"id"` + Armor string `yaml:"armor"` + } + type yamlRelease struct { + Format string `yaml:"format"` + Archives map[string]yamlArchive `yaml:"archives"` + PubKeys map[string]yamlPubKey `yaml:"public-keys"` + } + const yamlReleaseFormat = "v1" + + release := &Release{ + Path: baseDir, + Packages: make(map[string]*Package), + Archives: make(map[string]*Archive), + } + + fileName := stripBase(baseDir, filePath) + + yamlVar := yamlRelease{} + dec := yaml.NewDecoder(bytes.NewBuffer(data)) + dec.KnownFields(false) + err := dec.Decode(&yamlVar) + if err != nil { + return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) + } + if yamlVar.Format != yamlReleaseFormat { + return nil, fmt.Errorf("%s: expected format %q, got %q", fileName, yamlReleaseFormat, yamlVar.Format) + } + if len(yamlVar.Archives) == 0 { + return nil, fmt.Errorf("%s: no archives defined", fileName) + } + + // Decode the public keys and match against provided IDs. + pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) + for keyName, yamlPubKey := range yamlVar.PubKeys { + key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) + if err != nil { + return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) + } + if yamlPubKey.ID != key.KeyIdString() { + return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) + } + pubKeys[keyName] = key + } + + for archiveName, details := range yamlVar.Archives { + if details.Version == "" { + return nil, fmt.Errorf("%s: archive %q missing version field", fileName, archiveName) + } + if len(details.Suites) == 0 { + adjective := ubuntuAdjectives[details.Version] + if adjective == "" { + return nil, fmt.Errorf("%s: archive %q missing suites field", fileName, archiveName) + } + details.Suites = []string{adjective} + } + if len(details.Components) == 0 { + return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) + } + if len(yamlVar.Archives) == 1 { + details.Default = true + } else if details.Default && release.DefaultArchive != "" { + return nil, fmt.Errorf("%s: more than one default archive: %s, %s", fileName, release.DefaultArchive, archiveName) + } + if details.Default { + release.DefaultArchive = archiveName + } + if len(details.PubKeys) == 0 { + return nil, fmt.Errorf("%s: archive %q missing public-keys field", fileName, archiveName) + } + var archiveKeys []*packet.PublicKey + for _, keyName := range details.PubKeys { + key, ok := pubKeys[keyName] + if !ok { + return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) + } + archiveKeys = append(archiveKeys, key) + } + release.Archives[archiveName] = &Archive{ + Name: archiveName, + Version: details.Version, + Suites: details.Suites, + Components: details.Components, + PubKeys: archiveKeys, + } + } + + return release, err +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index e04f8eca..5c3cd885 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -13,7 +13,6 @@ import ( "gopkg.in/yaml.v3" "github.com/canonical/chisel/internal/deb" - "github.com/canonical/chisel/internal/pgputil" "github.com/canonical/chisel/internal/strdist" ) @@ -271,7 +270,6 @@ func readRelease(baseDir string) (*Release, error) { } return release, err } - func readSlices(release *Release, baseDir, dirName string) error { entries, err := os.ReadDir(dirName) if err != nil { @@ -318,20 +316,36 @@ func readSlices(release *Release, baseDir, dirName string) error { return nil } -type yamlRelease struct { - Format string `yaml:"format"` - Archives map[string]yamlArchive `yaml:"archives"` - PubKeys map[string]yamlPubKey `yaml:"v1-public-keys"` +var ubuntuAdjectives = map[string]string{ + "18.04": "bionic", + "20.04": "focal", + "22.04": "jammy", + "22.10": "kinetic", } -const yamlReleaseFormat = "chisel-v1" +func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { + type yamlFormat struct { + Format string `yaml:"format"` + } + + fileName := stripBase(baseDir, filePath) + + yamlVar := yamlFormat{} + dec := yaml.NewDecoder(bytes.NewBuffer(data)) + dec.KnownFields(false) + err := dec.Decode(&yamlVar) + if err != nil { + return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) + } -type yamlArchive struct { - Version string `yaml:"version"` - Suites []string `yaml:"suites"` - Components []string `yaml:"components"` - Default bool `yaml:"default"` - PubKeys []string `yaml:"v1-public-keys"` + switch yamlVar.Format { + case "chisel-v1": + return parseReleaseChiselV1(baseDir, filePath, data) + case "v1": + return parseReleaseV1(baseDir, filePath, data) + default: + return nil, fmt.Errorf("%s: unknown format %q", fileName, yamlVar.Format) + } } type yamlPackage struct { @@ -389,99 +403,6 @@ type yamlSlice struct { Mutate string `yaml:"mutate"` } -type yamlPubKey struct { - ID string `yaml:"id"` - Armor string `yaml:"armor"` -} - -var ubuntuAdjectives = map[string]string{ - "18.04": "bionic", - "20.04": "focal", - "22.04": "jammy", - "22.10": "kinetic", -} - -func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { - release := &Release{ - Path: baseDir, - Packages: make(map[string]*Package), - Archives: make(map[string]*Archive), - } - - fileName := stripBase(baseDir, filePath) - - yamlVar := yamlRelease{} - dec := yaml.NewDecoder(bytes.NewBuffer(data)) - dec.KnownFields(false) - err := dec.Decode(&yamlVar) - if err != nil { - return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) - } - if yamlVar.Format != yamlReleaseFormat { - return nil, fmt.Errorf("%s: expected format %q, got %q", fileName, yamlReleaseFormat, yamlVar.Format) - } - if len(yamlVar.Archives) == 0 { - return nil, fmt.Errorf("%s: no archives defined", fileName) - } - - // Decode the public keys and match against provided IDs. - pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) - for keyName, yamlPubKey := range yamlVar.PubKeys { - key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) - if err != nil { - return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) - } - if yamlPubKey.ID != key.KeyIdString() { - return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) - } - pubKeys[keyName] = key - } - - for archiveName, details := range yamlVar.Archives { - if details.Version == "" { - return nil, fmt.Errorf("%s: archive %q missing version field", fileName, archiveName) - } - if len(details.Suites) == 0 { - adjective := ubuntuAdjectives[details.Version] - if adjective == "" { - return nil, fmt.Errorf("%s: archive %q missing suites field", fileName, archiveName) - } - details.Suites = []string{adjective} - } - if len(details.Components) == 0 { - return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) - } - if len(yamlVar.Archives) == 1 { - details.Default = true - } else if details.Default && release.DefaultArchive != "" { - return nil, fmt.Errorf("%s: more than one default archive: %s, %s", fileName, release.DefaultArchive, archiveName) - } - if details.Default { - release.DefaultArchive = archiveName - } - if len(details.PubKeys) == 0 { - return nil, fmt.Errorf("%s: archive %q missing v1-public-keys field", fileName, archiveName) - } - var archiveKeys []*packet.PublicKey - for _, keyName := range details.PubKeys { - key, ok := pubKeys[keyName] - if !ok { - return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) - } - archiveKeys = append(archiveKeys, key) - } - release.Archives[archiveName] = &Archive{ - Name: archiveName, - Version: details.Version, - Suites: details.Suites, - Components: details.Components, - PubKeys: archiveKeys, - } - } - - return release, err -} - func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, error) { pkg := Package{ Name: pkgName, diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go index 9bbf444b..9a92089b 100644 --- a/internal/setup/setup_test.go +++ b/internal/setup/setup_test.go @@ -3,6 +3,7 @@ package setup_test import ( "os" "path/filepath" + "strings" "golang.org/x/crypto/openpgp/packet" . "gopkg.in/check.v1" @@ -33,7 +34,7 @@ var setupTests = []setupTest{{ format: foobar `, }, - relerror: `chisel.yaml: expected format "chisel-v1", got "foobar"`, + relerror: `chisel.yaml: unknown format "foobar"`, }, { summary: "Missing archives", input: map[string]string{ @@ -1030,7 +1031,28 @@ var defaultChiselYaml = ` ` func (s *S) TestParseRelease(c *C) { - for _, test := range setupTests { + // Run tests for format chisel-v1. + runParseReleaseTests(c, setupTests) + + // Run tests for format v1. + v1SetupTests := make([]setupTest, len(setupTests)) + for i, t := range setupTests { + t.relerror = strings.Replace(t.relerror, "chisel-v1", "v1", -1) + t.relerror = strings.Replace(t.relerror, "v1-public-keys", "public-keys", -1) + m := map[string]string{} + for k, v := range t.input { + v = strings.Replace(v, "chisel-v1", "v1", -1) + v = strings.Replace(v, "v1-public-keys", "public-keys", -1) + m[k] = v + } + t.input = m + v1SetupTests[i] = t + } + runParseReleaseTests(c, v1SetupTests) +} + +func runParseReleaseTests(c *C, tests []setupTest) { + for _, test := range tests { c.Logf("Summary: %s", test.summary) if _, ok := test.input["chisel.yaml"]; !ok { diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index 48e21bf1..1a19a144 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strings" . "gopkg.in/check.v1" @@ -562,7 +563,28 @@ func (a *testArchive) Exists(pkg string) bool { } func (s *S) TestRun(c *C) { - for _, test := range slicerTests { + // Run tests for format chisel-v1. + runSlicerTests(c, slicerTests) + + // Run tests for format v1. + v1SlicerTests := make([]slicerTest, len(slicerTests)) + for i, t := range slicerTests { + t.error = strings.Replace(t.error, "chisel-v1", "v1", -1) + t.error = strings.Replace(t.error, "v1-public-keys", "public-keys", -1) + m := map[string]string{} + for k, v := range t.release { + v = strings.Replace(v, "chisel-v1", "v1", -1) + v = strings.Replace(v, "v1-public-keys", "public-keys", -1) + m[k] = v + } + t.release = m + v1SlicerTests[i] = t + } + runSlicerTests(c, v1SlicerTests) +} + +func runSlicerTests(c *C, tests []slicerTest) { + for _, test := range tests { c.Logf("Summary: %s", test.summary) if _, ok := test.release["chisel.yaml"]; !ok { From 315e379dffac8b5f36acbd6e3eb27a37de8ce7a6 Mon Sep 17 00:00:00 2001 From: Alberto Carretero Date: Mon, 15 Jan 2024 12:07:17 +0100 Subject: [PATCH 2/2] refactor: unify format parsing --- internal/setup/releaseChiselV1.go | 110 ---------------------- internal/setup/releaseV1.go | 110 ---------------------- internal/setup/setup.go | 150 ++++++++++++++++++++++++------ 3 files changed, 122 insertions(+), 248 deletions(-) delete mode 100644 internal/setup/releaseChiselV1.go delete mode 100644 internal/setup/releaseV1.go diff --git a/internal/setup/releaseChiselV1.go b/internal/setup/releaseChiselV1.go deleted file mode 100644 index d5be1aa7..00000000 --- a/internal/setup/releaseChiselV1.go +++ /dev/null @@ -1,110 +0,0 @@ -package setup - -import ( - "bytes" - "fmt" - - "golang.org/x/crypto/openpgp/packet" - "gopkg.in/yaml.v3" - - "github.com/canonical/chisel/internal/pgputil" -) - -func parseReleaseChiselV1(baseDir, filePath string, data []byte) (*Release, error) { - type yamlArchive struct { - Version string `yaml:"version"` - Suites []string `yaml:"suites"` - Components []string `yaml:"components"` - Default bool `yaml:"default"` - PubKeys []string `yaml:"v1-public-keys"` - } - type yamlPubKey struct { - ID string `yaml:"id"` - Armor string `yaml:"armor"` - } - type yamlRelease struct { - Format string `yaml:"format"` - Archives map[string]yamlArchive `yaml:"archives"` - PubKeys map[string]yamlPubKey `yaml:"v1-public-keys"` - } - const yamlReleaseFormat = "chisel-v1" - - release := &Release{ - Path: baseDir, - Packages: make(map[string]*Package), - Archives: make(map[string]*Archive), - } - - fileName := stripBase(baseDir, filePath) - - yamlVar := yamlRelease{} - dec := yaml.NewDecoder(bytes.NewBuffer(data)) - dec.KnownFields(false) - err := dec.Decode(&yamlVar) - if err != nil { - return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) - } - if yamlVar.Format != yamlReleaseFormat { - return nil, fmt.Errorf("%s: expected format %q, got %q", fileName, yamlReleaseFormat, yamlVar.Format) - } - if len(yamlVar.Archives) == 0 { - return nil, fmt.Errorf("%s: no archives defined", fileName) - } - - // Decode the public keys and match against provided IDs. - pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) - for keyName, yamlPubKey := range yamlVar.PubKeys { - key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) - if err != nil { - return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) - } - if yamlPubKey.ID != key.KeyIdString() { - return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) - } - pubKeys[keyName] = key - } - - for archiveName, details := range yamlVar.Archives { - if details.Version == "" { - return nil, fmt.Errorf("%s: archive %q missing version field", fileName, archiveName) - } - if len(details.Suites) == 0 { - adjective := ubuntuAdjectives[details.Version] - if adjective == "" { - return nil, fmt.Errorf("%s: archive %q missing suites field", fileName, archiveName) - } - details.Suites = []string{adjective} - } - if len(details.Components) == 0 { - return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) - } - if len(yamlVar.Archives) == 1 { - details.Default = true - } else if details.Default && release.DefaultArchive != "" { - return nil, fmt.Errorf("%s: more than one default archive: %s, %s", fileName, release.DefaultArchive, archiveName) - } - if details.Default { - release.DefaultArchive = archiveName - } - if len(details.PubKeys) == 0 { - return nil, fmt.Errorf("%s: archive %q missing v1-public-keys field", fileName, archiveName) - } - var archiveKeys []*packet.PublicKey - for _, keyName := range details.PubKeys { - key, ok := pubKeys[keyName] - if !ok { - return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) - } - archiveKeys = append(archiveKeys, key) - } - release.Archives[archiveName] = &Archive{ - Name: archiveName, - Version: details.Version, - Suites: details.Suites, - Components: details.Components, - PubKeys: archiveKeys, - } - } - - return release, err -} diff --git a/internal/setup/releaseV1.go b/internal/setup/releaseV1.go deleted file mode 100644 index 5ea46071..00000000 --- a/internal/setup/releaseV1.go +++ /dev/null @@ -1,110 +0,0 @@ -package setup - -import ( - "bytes" - "fmt" - - "golang.org/x/crypto/openpgp/packet" - "gopkg.in/yaml.v3" - - "github.com/canonical/chisel/internal/pgputil" -) - -func parseReleaseV1(baseDir, filePath string, data []byte) (*Release, error) { - type yamlArchive struct { - Version string `yaml:"version"` - Suites []string `yaml:"suites"` - Components []string `yaml:"components"` - Default bool `yaml:"default"` - PubKeys []string `yaml:"public-keys"` - } - type yamlPubKey struct { - ID string `yaml:"id"` - Armor string `yaml:"armor"` - } - type yamlRelease struct { - Format string `yaml:"format"` - Archives map[string]yamlArchive `yaml:"archives"` - PubKeys map[string]yamlPubKey `yaml:"public-keys"` - } - const yamlReleaseFormat = "v1" - - release := &Release{ - Path: baseDir, - Packages: make(map[string]*Package), - Archives: make(map[string]*Archive), - } - - fileName := stripBase(baseDir, filePath) - - yamlVar := yamlRelease{} - dec := yaml.NewDecoder(bytes.NewBuffer(data)) - dec.KnownFields(false) - err := dec.Decode(&yamlVar) - if err != nil { - return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) - } - if yamlVar.Format != yamlReleaseFormat { - return nil, fmt.Errorf("%s: expected format %q, got %q", fileName, yamlReleaseFormat, yamlVar.Format) - } - if len(yamlVar.Archives) == 0 { - return nil, fmt.Errorf("%s: no archives defined", fileName) - } - - // Decode the public keys and match against provided IDs. - pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) - for keyName, yamlPubKey := range yamlVar.PubKeys { - key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) - if err != nil { - return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) - } - if yamlPubKey.ID != key.KeyIdString() { - return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) - } - pubKeys[keyName] = key - } - - for archiveName, details := range yamlVar.Archives { - if details.Version == "" { - return nil, fmt.Errorf("%s: archive %q missing version field", fileName, archiveName) - } - if len(details.Suites) == 0 { - adjective := ubuntuAdjectives[details.Version] - if adjective == "" { - return nil, fmt.Errorf("%s: archive %q missing suites field", fileName, archiveName) - } - details.Suites = []string{adjective} - } - if len(details.Components) == 0 { - return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) - } - if len(yamlVar.Archives) == 1 { - details.Default = true - } else if details.Default && release.DefaultArchive != "" { - return nil, fmt.Errorf("%s: more than one default archive: %s, %s", fileName, release.DefaultArchive, archiveName) - } - if details.Default { - release.DefaultArchive = archiveName - } - if len(details.PubKeys) == 0 { - return nil, fmt.Errorf("%s: archive %q missing public-keys field", fileName, archiveName) - } - var archiveKeys []*packet.PublicKey - for _, keyName := range details.PubKeys { - key, ok := pubKeys[keyName] - if !ok { - return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) - } - archiveKeys = append(archiveKeys, key) - } - release.Archives[archiveName] = &Archive{ - Name: archiveName, - Version: details.Version, - Suites: details.Suites, - Components: details.Components, - PubKeys: archiveKeys, - } - } - - return release, err -} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 5c3cd885..fe142d92 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v3" "github.com/canonical/chisel/internal/deb" + "github.com/canonical/chisel/internal/pgputil" "github.com/canonical/chisel/internal/strdist" ) @@ -270,6 +271,7 @@ func readRelease(baseDir string) (*Release, error) { } return release, err } + func readSlices(release *Release, baseDir, dirName string) error { entries, err := os.ReadDir(dirName) if err != nil { @@ -316,36 +318,22 @@ func readSlices(release *Release, baseDir, dirName string) error { return nil } -var ubuntuAdjectives = map[string]string{ - "18.04": "bionic", - "20.04": "focal", - "22.04": "jammy", - "22.10": "kinetic", +type yamlRelease struct { + Format string `yaml:"format"` + Archives map[string]yamlArchive `yaml:"archives"` + PubKeys map[string]yamlPubKey `yaml:"public-keys"` + // V1PubKeys is used for compatibility with format "chisel-v1". + V1PubKeys map[string]yamlPubKey `yaml:"v1-public-keys"` } -func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { - type yamlFormat struct { - Format string `yaml:"format"` - } - - fileName := stripBase(baseDir, filePath) - - yamlVar := yamlFormat{} - dec := yaml.NewDecoder(bytes.NewBuffer(data)) - dec.KnownFields(false) - err := dec.Decode(&yamlVar) - if err != nil { - return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) - } - - switch yamlVar.Format { - case "chisel-v1": - return parseReleaseChiselV1(baseDir, filePath, data) - case "v1": - return parseReleaseV1(baseDir, filePath, data) - default: - return nil, fmt.Errorf("%s: unknown format %q", fileName, yamlVar.Format) - } +type yamlArchive struct { + Version string `yaml:"version"` + Suites []string `yaml:"suites"` + Components []string `yaml:"components"` + Default bool `yaml:"default"` + PubKeys []string `yaml:"public-keys"` + // V1PubKeys is used for compatibility with format "chisel-v1". + V1PubKeys []string `yaml:"v1-public-keys"` } type yamlPackage struct { @@ -403,6 +391,112 @@ type yamlSlice struct { Mutate string `yaml:"mutate"` } +type yamlPubKey struct { + ID string `yaml:"id"` + Armor string `yaml:"armor"` +} + +var ubuntuAdjectives = map[string]string{ + "18.04": "bionic", + "20.04": "focal", + "22.04": "jammy", + "22.10": "kinetic", +} + +func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { + release := &Release{ + Path: baseDir, + Packages: make(map[string]*Package), + Archives: make(map[string]*Archive), + } + + fileName := stripBase(baseDir, filePath) + + yamlVar := yamlRelease{} + dec := yaml.NewDecoder(bytes.NewBuffer(data)) + dec.KnownFields(false) + err := dec.Decode(&yamlVar) + if err != nil { + return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) + } + if yamlVar.Format != "chisel-v1" && yamlVar.Format != "v1" { + return nil, fmt.Errorf("%s: unknown format %q", fileName, yamlVar.Format) + } + // If format is "chisel-v1" we have to translate from the yaml key "v1-public-keys" to + // "public-keys". + if yamlVar.Format == "chisel-v1" { + yamlVar.PubKeys = yamlVar.V1PubKeys + for name, details := range yamlVar.Archives { + details.PubKeys = details.V1PubKeys + yamlVar.Archives[name] = details + } + } + if len(yamlVar.Archives) == 0 { + return nil, fmt.Errorf("%s: no archives defined", fileName) + } + + // Decode the public keys and match against provided IDs. + pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) + for keyName, yamlPubKey := range yamlVar.PubKeys { + key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) + if err != nil { + return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) + } + if yamlPubKey.ID != key.KeyIdString() { + return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) + } + pubKeys[keyName] = key + } + + for archiveName, details := range yamlVar.Archives { + if details.Version == "" { + return nil, fmt.Errorf("%s: archive %q missing version field", fileName, archiveName) + } + if len(details.Suites) == 0 { + adjective := ubuntuAdjectives[details.Version] + if adjective == "" { + return nil, fmt.Errorf("%s: archive %q missing suites field", fileName, archiveName) + } + details.Suites = []string{adjective} + } + if len(details.Components) == 0 { + return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) + } + if len(yamlVar.Archives) == 1 { + details.Default = true + } else if details.Default && release.DefaultArchive != "" { + return nil, fmt.Errorf("%s: more than one default archive: %s, %s", fileName, release.DefaultArchive, archiveName) + } + if details.Default { + release.DefaultArchive = archiveName + } + if len(details.PubKeys) == 0 { + if yamlVar.Format == "chisel-v1" { + return nil, fmt.Errorf("%s: archive %q missing v1-public-keys field", fileName, archiveName) + } else { + return nil, fmt.Errorf("%s: archive %q missing public-keys field", fileName, archiveName) + } + } + var archiveKeys []*packet.PublicKey + for _, keyName := range details.PubKeys { + key, ok := pubKeys[keyName] + if !ok { + return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) + } + archiveKeys = append(archiveKeys, key) + } + release.Archives[archiveName] = &Archive{ + Name: archiveName, + Version: details.Version, + Suites: details.Suites, + Components: details.Components, + PubKeys: archiveKeys, + } + } + + return release, err +} + func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, error) { pkg := Package{ Name: pkgName,