From 607fbd5e63ead17952fbfce87884b6ee2410a6c2 Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 17:38:22 +0100 Subject: [PATCH 01/14] fix(conflicts): resolve merge conflicts --- pkg/commands/compute/compute_mocks_test.go | 13 ++ pkg/commands/compute/deploy.go | 16 +- pkg/commands/compute/deploy_test.go | 156 +++++++++--------- .../setup/{dictionary.go => config_store.go} | 71 ++++---- pkg/manifest/manifest.go | 16 +- 5 files changed, 140 insertions(+), 132 deletions(-) rename pkg/commands/compute/setup/{dictionary.go => config_store.go} (59%) diff --git a/pkg/commands/compute/compute_mocks_test.go b/pkg/commands/compute/compute_mocks_test.go index f4cd2e141..ac21c06cc 100644 --- a/pkg/commands/compute/compute_mocks_test.go +++ b/pkg/commands/compute/compute_mocks_test.go @@ -32,6 +32,19 @@ func createBackendOK(i *fastly.CreateBackendInput) (*fastly.Backend, error) { }, nil } +func createConfigStoreOK(i *fastly.CreateConfigStoreInput) (*fastly.ConfigStore, error) { + return &fastly.ConfigStore{ + Name: i.Name, + }, nil +} + +func createConfigStoreItemOK(i *fastly.CreateConfigStoreItemInput) (*fastly.ConfigStoreItem, error) { + return &fastly.ConfigStoreItem{ + Key: i.Key, + Value: i.Value, + }, nil +} + func createDictionaryOK(i *fastly.CreateDictionaryInput) (*fastly.Dictionary, error) { return &fastly.Dictionary{ ServiceID: i.ServiceID, diff --git a/pkg/commands/compute/deploy.go b/pkg/commands/compute/deploy.go index da90d36ee..48addcad8 100644 --- a/pkg/commands/compute/deploy.go +++ b/pkg/commands/compute/deploy.go @@ -846,7 +846,7 @@ func pkgUpload(spinner text.Spinner, client api.Interface, serviceID string, ver type setupObjects struct { domains *setup.Domains backends *setup.Backends - dictionaries *setup.Dictionaries + configStores *setup.ConfigStores loggers *setup.Loggers kvStores *setup.KVStores secretStores *setup.SecretStores @@ -907,13 +907,13 @@ func constructSetupObjects( Stdout: out, } - so.dictionaries = &setup.Dictionaries{ + so.configStores = &setup.ConfigStores{ APIClient: c.Globals.APIClient, AcceptDefaults: c.Globals.Flags.AcceptDefaults, NonInteractive: c.Globals.Flags.NonInteractive, ServiceID: serviceID, ServiceVersion: serviceVersion, - Setup: c.Manifest.File.Setup.Dictionaries, + Setup: c.Manifest.File.Setup.ConfigStores, Stdin: in, Stdout: out, } @@ -976,10 +976,10 @@ func processSetupConfig( return fmt.Errorf("error configuring service backends: %w", err) } - if so.dictionaries.Predefined() { - if err := so.dictionaries.Configure(); err != nil { + if so.configStores.Predefined() { + if err := so.configStores.Configure(); err != nil { errLogService(c.Globals.ErrLog, err, serviceID, serviceVersion) - return fmt.Errorf("error configuring service dictionaries: %w", err) + return fmt.Errorf("error configuring service config stores: %w", err) } } @@ -1038,7 +1038,7 @@ func processSetupCreation( // We presume if we're dealing with newService they have been set. if newService { so.backends.Spinner = spinner - so.dictionaries.Spinner = spinner + so.configStores.Spinner = spinner so.kvStores.Spinner = spinner so.secretStores.Spinner = spinner @@ -1053,7 +1053,7 @@ func processSetupCreation( return err } - if err := so.dictionaries.Create(); err != nil { + if err := so.configStores.Create(); err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Accept defaults": c.Globals.Flags.AcceptDefaults, "Auto-yes": c.Globals.Flags.AutoYes, diff --git a/pkg/commands/compute/deploy_test.go b/pkg/commands/compute/deploy_test.go index c21d59d6d..8a71c06b0 100644 --- a/pkg/commands/compute/deploy_test.go +++ b/pkg/commands/compute/deploy_test.go @@ -1160,7 +1160,7 @@ func TestDeploy(t *testing.T) { }, }, { - name: "success with setup.dictionaries configuration and existing service", + name: "success with setup.config_stores configuration and existing service", args: args("compute deploy --service-id 123 --token 123"), api: mock.API{ ActivateVersionFn: activateVersionOk, @@ -1188,12 +1188,12 @@ func TestDeploy(t *testing.T) { manifest_version = 2 language = "rust" - [setup.dictionaries.dict_a] + [setup.config_stores.example] description = "My first dictionary" - [setup.dictionaries.dict_a.items.foo] + [setup.config_stores.example.items.foo] value = "my default value for foo" description = "a good description about foo" - [setup.dictionaries.dict_a.items.bar] + [setup.config_stores.example.items.bar] value = "my default value for bar" description = "a good description about bar" `, @@ -1204,29 +1204,29 @@ func TestDeploy(t *testing.T) { }, dontWantOutput: []string{ "Configuring dictionary 'dict_a'", - "Create a dictionary key called 'foo'", - "Create a dictionary key called 'bar'", - "Creating dictionary 'dict_a'", - "Creating dictionary item 'foo'", - "Creating dictionary item 'bar'", + "Create a config store key called 'foo'", + "Create a config store key called 'bar'", + "Creating config store 'example'", + "Creating config store item 'foo'", + "Creating config store item 'bar'", }, }, { - name: "success with setup.dictionaries configuration and no existing service", + name: "success with setup.config_stores configuration and no existing service", args: args("compute deploy --token 123"), api: mock.API{ - ActivateVersionFn: activateVersionOk, - CreateBackendFn: createBackendOK, - CreateDictionaryFn: createDictionaryOK, - CreateDictionaryItemFn: createDictionaryItemOK, - CreateDomainFn: createDomainOK, - CreateServiceFn: createServiceOK, - GetPackageFn: getPackageOk, - GetServiceFn: getServiceOK, - GetServiceDetailsFn: getServiceDetailsWasm, - ListDomainsFn: listDomainsOk, - ListVersionsFn: testutil.ListVersions, - UpdatePackageFn: updatePackageOk, + ActivateVersionFn: activateVersionOk, + CreateBackendFn: createBackendOK, + CreateConfigStoreFn: createConfigStoreOK, + CreateConfigStoreItemFn: createConfigStoreItemOK, + CreateDomainFn: createDomainOK, + CreateServiceFn: createServiceOK, + GetPackageFn: getPackageOk, + GetServiceFn: getServiceOK, + GetServiceDetailsFn: getServiceDetailsWasm, + ListDomainsFn: listDomainsOk, + ListVersionsFn: testutil.ListVersions, + UpdatePackageFn: updatePackageOk, }, httpClientRes: []*http.Response{ { @@ -1243,12 +1243,12 @@ func TestDeploy(t *testing.T) { manifest_version = 2 language = "rust" - [setup.dictionaries.dict_a] - description = "My first dictionary" - [setup.dictionaries.dict_a.items.foo] + [setup.config_stores.example] + description = "My first store" + [setup.config_stores.example.items.foo] value = "my default value for foo" description = "a good description about foo" - [setup.dictionaries.dict_a.items.bar] + [setup.config_stores.example.items.bar] value = "my default value for bar" description = "a good description about bar" `, @@ -1256,36 +1256,36 @@ func TestDeploy(t *testing.T) { "Y", // when prompted to create a new service }, wantOutput: []string{ - "Configuring dictionary 'dict_a'", - "My first dictionary", - "Create a dictionary key called 'foo'", + "Configuring config store 'example'", + "My first store", + "Create a config store key called 'foo'", "my default value for foo", - "Create a dictionary key called 'bar'", + "Create a config store key called 'bar'", "my default value for bar", - "Creating dictionary 'dict_a'", - "Creating dictionary item 'foo'", - "Creating dictionary item 'bar'", + "Creating config store 'example'", + "Creating config store item 'foo'", + "Creating config store item 'bar'", "Uploading package", "Activating service", "SUCCESS: Deployed package (service 12345, version 1)", }, }, { - name: "success with setup.dictionaries configuration and no existing service and --non-interactive", + name: "success with setup.config_stores configuration and no existing service and --non-interactive", args: args("compute deploy --non-interactive --token 123"), api: mock.API{ - ActivateVersionFn: activateVersionOk, - CreateBackendFn: createBackendOK, - CreateDictionaryFn: createDictionaryOK, - CreateDictionaryItemFn: createDictionaryItemOK, - CreateDomainFn: createDomainOK, - CreateServiceFn: createServiceOK, - GetPackageFn: getPackageOk, - GetServiceFn: getServiceOK, - GetServiceDetailsFn: getServiceDetailsWasm, - ListDomainsFn: listDomainsOk, - ListVersionsFn: testutil.ListVersions, - UpdatePackageFn: updatePackageOk, + ActivateVersionFn: activateVersionOk, + CreateBackendFn: createBackendOK, + CreateConfigStoreFn: createConfigStoreOK, + CreateConfigStoreItemFn: createConfigStoreItemOK, + CreateDomainFn: createDomainOK, + CreateServiceFn: createServiceOK, + GetPackageFn: getPackageOk, + GetServiceFn: getServiceOK, + GetServiceDetailsFn: getServiceDetailsWasm, + ListDomainsFn: listDomainsOk, + ListVersionsFn: testutil.ListVersions, + UpdatePackageFn: updatePackageOk, }, httpClientRes: []*http.Response{ { @@ -1302,12 +1302,12 @@ func TestDeploy(t *testing.T) { manifest_version = 2 language = "rust" - [setup.dictionaries.dict_a] - description = "My first dictionary" - [setup.dictionaries.dict_a.items.foo] + [setup.config_stores.example] + description = "My first store" + [setup.config_stores.example.items.foo] value = "my default value for foo" description = "a good description about foo" - [setup.dictionaries.dict_a.items.bar] + [setup.config_stores.example.items.bar] value = "my default value for bar" description = "a good description about bar" `, @@ -1315,30 +1315,30 @@ func TestDeploy(t *testing.T) { "Y", // when prompted to create a new service }, wantOutput: []string{ - "Creating dictionary 'dict_a'", - "Creating dictionary item 'foo'", - "Creating dictionary item 'bar'", + "Creating config store 'example'", + "Creating config store item 'foo'", + "Creating config store item 'bar'", "Uploading package", "Activating service", "SUCCESS: Deployed package (service 12345, version 1)", }, }, { - name: "success with setup.dictionaries configuration and no existing service and no predefined values", + name: "success with setup.config_stores configuration and no existing service and no predefined values", args: args("compute deploy --token 123"), api: mock.API{ - ActivateVersionFn: activateVersionOk, - CreateBackendFn: createBackendOK, - CreateDictionaryFn: createDictionaryOK, - CreateDictionaryItemFn: createDictionaryItemOK, - CreateDomainFn: createDomainOK, - CreateServiceFn: createServiceOK, - GetPackageFn: getPackageOk, - GetServiceFn: getServiceOK, - GetServiceDetailsFn: getServiceDetailsWasm, - ListDomainsFn: listDomainsOk, - ListVersionsFn: testutil.ListVersions, - UpdatePackageFn: updatePackageOk, + ActivateVersionFn: activateVersionOk, + CreateBackendFn: createBackendOK, + CreateConfigStoreFn: createConfigStoreOK, + CreateConfigStoreItemFn: createConfigStoreItemOK, + CreateDomainFn: createDomainOK, + CreateServiceFn: createServiceOK, + GetPackageFn: getPackageOk, + GetServiceFn: getServiceOK, + GetServiceDetailsFn: getServiceDetailsWasm, + ListDomainsFn: listDomainsOk, + ListVersionsFn: testutil.ListVersions, + UpdatePackageFn: updatePackageOk, }, httpClientRes: []*http.Response{ { @@ -1355,30 +1355,30 @@ func TestDeploy(t *testing.T) { manifest_version = 2 language = "rust" - [setup.dictionaries.dict_a] - [setup.dictionaries.dict_a.items.foo] - [setup.dictionaries.dict_a.items.bar] + [setup.config_stores.example] + [setup.config_stores.example.items.foo] + [setup.config_stores.example.items.bar] `, stdin: []string{ "Y", // when prompted to create a new service }, wantOutput: []string{ - "Configuring dictionary 'dict_a'", - "Create a dictionary key called 'foo'", - "Create a dictionary key called 'bar'", - "Creating dictionary 'dict_a'", - "Creating dictionary item 'foo'", - "Creating dictionary item 'bar'", + "Configuring config store 'example'", + "Create a config store key called 'foo'", + "Create a config store key called 'bar'", + "Creating config store 'example'", + "Creating config store item 'foo'", + "Creating config store item 'bar'", "Uploading package", "Activating service", "SUCCESS: Deployed package (service 12345, version 1)", }, // The following are predefined values for the `description` and `value` - // fields from the prior setup.dictionaries tests that we expect to not - // be present in the stdout/stderr as the [setup/dictionaries] + // fields from the prior setup.config_stores tests that we expect to not + // be present in the stdout/stderr as the [setup.config_stores] // configuration does not define them. dontWantOutput: []string{ - "My first dictionary", + "My first store", "my default value for foo", "my default value for bar", }, diff --git a/pkg/commands/compute/setup/dictionary.go b/pkg/commands/compute/setup/config_store.go similarity index 59% rename from pkg/commands/compute/setup/dictionary.go rename to pkg/commands/compute/setup/config_store.go index b3e9a32e0..9774ea785 100644 --- a/pkg/commands/compute/setup/dictionary.go +++ b/pkg/commands/compute/setup/config_store.go @@ -11,11 +11,11 @@ import ( "github.com/fastly/go-fastly/v8/fastly" ) -// Dictionaries represents the service state related to dictionaries defined +// ConfigStores represents the service state related to config stores defined // within the fastly.toml [setup] configuration. // // NOTE: It implements the setup.Interface interface. -type Dictionaries struct { +type ConfigStores struct { // Public APIClient api.Interface AcceptDefaults bool @@ -23,42 +23,40 @@ type Dictionaries struct { Spinner text.Spinner ServiceID string ServiceVersion int - Setup map[string]*manifest.SetupDictionary + Setup map[string]*manifest.SetupConfigStore Stdin io.Reader Stdout io.Writer // Private - required []Dictionary + required []ConfigStore } -// Dictionary represents the configuration parameters for creating a dictionary -// via the API client. -// -// NOTE: WriteOnly (i.e. private) dictionaries not supported. -type Dictionary struct { +// ConfigStore represents the configuration parameters for creating a config +// store via the API client. +type ConfigStore struct { Name string - Items []DictionaryItem + Items []ConfigStoreItem } -// DictionaryItem represents the configuration parameters for creating dictionary -// items via the API client. -type DictionaryItem struct { +// ConfigStoreItem represents the configuration parameters for creating config +// store items via the API client. +type ConfigStoreItem struct { Key string Value string } // Configure prompts the user for specific values related to the service resource. -func (d *Dictionaries) Configure() error { +func (d *ConfigStores) Configure() error { for name, settings := range d.Setup { if !d.AcceptDefaults && !d.NonInteractive { text.Break(d.Stdout) - text.Output(d.Stdout, "Configuring dictionary '%s'", name) + text.Output(d.Stdout, "Configuring config store '%s'", name) if settings.Description != "" { text.Output(d.Stdout, settings.Description) } } - var items []DictionaryItem + var items []ConfigStoreItem for key, item := range settings.Items { dv := "example" @@ -74,7 +72,7 @@ func (d *Dictionaries) Configure() error { if !d.AcceptDefaults && !d.NonInteractive { text.Break(d.Stdout) - text.Output(d.Stdout, "Create a dictionary key called '%s'", key) + text.Output(d.Stdout, "Create a config store key called '%s'", key) if item.Description != "" { text.Output(d.Stdout, item.Description) } @@ -90,13 +88,13 @@ func (d *Dictionaries) Configure() error { value = dv } - items = append(items, DictionaryItem{ + items = append(items, ConfigStoreItem{ Key: key, Value: value, }) } - d.required = append(d.required, Dictionary{ + d.required = append(d.required, ConfigStore{ Name: name, Items: items, }) @@ -106,26 +104,24 @@ func (d *Dictionaries) Configure() error { } // Create calls the relevant API to create the service resource(s). -func (d *Dictionaries) Create() error { +func (d *ConfigStores) Create() error { if d.Spinner == nil { return errors.RemediationError{ - Inner: fmt.Errorf("internal logic error: no text.Progress configured for setup.Dictionaries"), + Inner: fmt.Errorf("internal logic error: no spinner configured for setup.ConfigStores"), Remediation: errors.BugRemediation, } } - for _, dictionary := range d.required { + for _, store := range d.required { err := d.Spinner.Start() if err != nil { return err } - msg := fmt.Sprintf("Creating dictionary '%s'", dictionary.Name) + msg := fmt.Sprintf("Creating config store '%s'", store.Name) d.Spinner.Message(msg + "...") - dict, err := d.APIClient.CreateDictionary(&fastly.CreateDictionaryInput{ - ServiceID: d.ServiceID, - ServiceVersion: d.ServiceVersion, - Name: &dictionary.Name, + cs, err := d.APIClient.CreateConfigStore(&fastly.CreateConfigStoreInput{ + Name: store.Name, }) if err != nil { d.Spinner.StopFailMessage(msg) @@ -133,7 +129,7 @@ func (d *Dictionaries) Create() error { if err != nil { return err } - return fmt.Errorf("error creating dictionary: %w", err) + return fmt.Errorf("error creating config store: %w", err) } d.Spinner.StopMessage(msg) @@ -142,20 +138,19 @@ func (d *Dictionaries) Create() error { return err } - if len(dictionary.Items) > 0 { - for _, item := range dictionary.Items { + if len(store.Items) > 0 { + for _, item := range store.Items { err := d.Spinner.Start() if err != nil { return err } - msg := fmt.Sprintf("Creating dictionary item '%s'", item.Key) + msg := fmt.Sprintf("Creating config store item '%s'", item.Key) d.Spinner.Message(msg + "...") - _, err = d.APIClient.CreateDictionaryItem(&fastly.CreateDictionaryItemInput{ - ServiceID: d.ServiceID, - DictionaryID: dict.ID, - ItemKey: item.Key, - ItemValue: item.Value, + _, err = d.APIClient.CreateConfigStoreItem(&fastly.CreateConfigStoreItemInput{ + StoreID: cs.ID, + Key: item.Key, + Value: item.Value, }) if err != nil { d.Spinner.StopFailMessage(msg) @@ -163,7 +158,7 @@ func (d *Dictionaries) Create() error { if err != nil { return err } - return fmt.Errorf("error creating dictionary item: %w", err) + return fmt.Errorf("error creating config store item: %w", err) } d.Spinner.StopMessage(msg) @@ -180,6 +175,6 @@ func (d *Dictionaries) Create() error { // Predefined indicates if the service resource has been specified within the // fastly.toml file using a [setup] configuration block. -func (d *Dictionaries) Predefined() bool { +func (d *ConfigStores) Predefined() bool { return len(d.Setup) > 0 } diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index b2cc7ab5a..f1417ab32 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -219,7 +219,7 @@ type Scripts struct { // the package. See https://developer.fastly.com/reference/fastly-toml/. type Setup struct { Backends map[string]*SetupBackend `toml:"backends,omitempty"` - Dictionaries map[string]*SetupDictionary `toml:"dictionaries,omitempty"` + ConfigStores map[string]*SetupConfigStore `toml:"config_stores,omitempty"` Loggers map[string]*SetupLogger `toml:"log_endpoints,omitempty"` KVStores map[string]*SetupKVStore `toml:"kv_stores,omitempty"` SecretStores map[string]*SetupSecretStore `toml:"secret_stores,omitempty"` @@ -232,7 +232,7 @@ func (s Setup) Defined() bool { if len(s.Backends) > 0 { defined = true } - if len(s.Dictionaries) > 0 { + if len(s.ConfigStores) > 0 { defined = true } if len(s.Loggers) > 0 { @@ -252,14 +252,14 @@ type SetupBackend struct { Description string `toml:"description,omitempty"` } -// SetupDictionary represents a '[setup.dictionaries.]' instance. -type SetupDictionary struct { - Items map[string]SetupDictionaryItems `toml:"items,omitempty"` - Description string `toml:"description,omitempty"` +// SetupConfigStore represents a '[setup.dictionaries.]' instance. +type SetupConfigStore struct { + Items map[string]SetupConfigStoreItems `toml:"items,omitempty"` + Description string `toml:"description,omitempty"` } -// SetupDictionaryItems represents a '[setup.dictionaries..items]' instance. -type SetupDictionaryItems struct { +// SetupConfigStoreItems represents a '[setup.dictionaries..items]' instance. +type SetupConfigStoreItems struct { Value string `toml:"value,omitempty"` Description string `toml:"description,omitempty"` } From f4082710263b998fab8812b8fbe5f6c18c3e3b0e Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 14:18:27 +0100 Subject: [PATCH 02/14] fix(errors): replace old release with current ref link --- pkg/errors/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index c0c0024e1..11c3f8b0d 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -68,7 +68,7 @@ var ErrInvalidManifestVersion = RemediationError{ // longer compatible with the current CLI version. var ErrIncompatibleManifestVersion = RemediationError{ Inner: fmt.Errorf("the fastly.toml contains an incompatible manifest_version number"), - Remediation: "Update the `manifest_version` in the fastly.toml and refer to https://github.com/fastly/cli/releases/tag/v0.39.3 for changes to the manifest structure", + Remediation: "Update the `manifest_version` in the fastly.toml and refer to https://developer.fastly.com/reference/compute/fastly-toml/ for changes to the manifest structure", } // ErrNoID means no --id value has been provided. From da4dbafaa04a40c1a17ad7ce62ea9dc73acddf3d Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 14:29:11 +0100 Subject: [PATCH 03/14] refactor(manifest): remove fix for an old bug from 0.25.0 --- pkg/manifest/manifest.go | 75 ------------------- pkg/manifest/manifest_test.go | 5 -- .../fastly-invalid-section-version.toml | 5 -- 3 files changed, 85 deletions(-) delete mode 100644 pkg/manifest/testdata/fastly-invalid-section-version.toml diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index f1417ab32..93aee2405 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -1,8 +1,6 @@ package manifest import ( - "bufio" - "bytes" "fmt" "io" "os" @@ -493,30 +491,6 @@ func (f *File) Read(path string) (err error) { return err } - // NOTE: temporary fix needed because of a bug that appeared in v0.25.0 where - // the manifest_version was stored in fastly.toml as a 'section', e.g. - // `[manifest_version]`. - // - // This subsequently would cause errors when trying to unmarshal the data, so - // we need to identify if it exists in the file (as a section) and remove it. - // - // We do this before trying to unmarshal the toml data into a go data - // structure otherwise we'll see errors from the toml library. - manifestSection, err := containsManifestSection(data) - if err != nil { - f.logErr(err) - return fmt.Errorf("failed to parse the fastly.toml manifest: %w", err) - } - - if manifestSection { - buf, err := stripManifestSection(bytes.NewReader(data), path) - if err != nil { - f.logErr(err) - return fsterr.ErrInvalidManifestVersion - } - data = buf.Bytes() - } - // The AutoMigrateVersion() method will either return the []byte unmodified or // it will have updated the manifest_version field to reflect the latest // version supported by the Fastly CLI. @@ -582,55 +556,6 @@ func (f *File) Write(path string) error { return fp.Close() } -// containsManifestSection loads the slice of bytes into a toml tree structure -// before checking if the manifest_version is defined as a toml section block. -func containsManifestSection(data []byte) (bool, error) { - tree, err := toml.LoadBytes(data) - if err != nil { - return false, err - } - - if _, ok := tree.GetArray("manifest_version").(*toml.Tree); ok { - return true, nil - } - - return false, nil -} - -// stripManifestSection reads the manifest line-by-line storing the lines that -// don't contain `[manifest_version]` into a buffer to be written back to disk. -// -// It would've been better if we could have relied on the toml library to delete -// the section but unfortunately that means it would end up deleting the entire -// block and not just the key specified. Meaning if the manifest_version key -// was in the middle of the manifest with other keys below it, deleting the -// manifest_version would cause all keys below it to be deleted as they would -// all be considered part of that section block. -func stripManifestSection(r io.Reader, path string) (*bytes.Buffer, error) { - var data []byte - buf := bytes.NewBuffer(data) - - scanner := bufio.NewScanner(r) - for scanner.Scan() { - if scanner.Text() != "[manifest_version]" { - _, err := buf.Write(scanner.Bytes()) - if err != nil { - return buf, err - } - _, err = buf.WriteString("\n") - if err != nil { - return buf, err - } - } - } - if err := scanner.Err(); err != nil { - return buf, err - } - - err := os.WriteFile(path, buf.Bytes(), FilePermissions) - return buf, err -} - // appendSpecRef appends the fastly.toml specification URL to the manifest. func appendSpecRef(w io.Writer) error { s := fmt.Sprintf("# %s\n# %s\n\n", SpecIntro, SpecURL) diff --git a/pkg/manifest/manifest_test.go b/pkg/manifest/manifest_test.go index 3a355cc63..b5b0b0ca8 100644 --- a/pkg/manifest/manifest_test.go +++ b/pkg/manifest/manifest_test.go @@ -35,10 +35,6 @@ func TestManifest(t *testing.T) { manifest: "fastly-invalid-missing-version.toml", valid: true, // expect manifest_version to be set to latest version }, - "invalid: manifest_version as a section": { - manifest: "fastly-invalid-section-version.toml", - valid: true, // expect manifest_version to be set to latest version - }, "invalid: manifest_version Atoi error": { manifest: "fastly-invalid-unrecognised.toml", valid: false, @@ -63,7 +59,6 @@ func TestManifest(t *testing.T) { "fastly-valid-semver.toml", "fastly-valid-integer.toml", "fastly-invalid-missing-version.toml", - "fastly-invalid-section-version.toml", "fastly-invalid-unrecognised.toml", "fastly-invalid-version-exceeded.toml", } { diff --git a/pkg/manifest/testdata/fastly-invalid-section-version.toml b/pkg/manifest/testdata/fastly-invalid-section-version.toml deleted file mode 100644 index e62eeacdd..000000000 --- a/pkg/manifest/testdata/fastly-invalid-section-version.toml +++ /dev/null @@ -1,5 +0,0 @@ -name = "Default Rust template" -description = "Default package template for Rust based edge compute projects." -[manifest_version] -authors = ["phamann "] -language = "rust" From ca75bbb35f6179c16c3b5b47ee0f46c15cd8e17e Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 17:40:49 +0100 Subject: [PATCH 04/14] fix(conflicts): resolve merge conflicts 2 --- pkg/manifest/data.go | 67 +++++ pkg/manifest/file.go | 278 ++++++++++++++++++ pkg/manifest/flags.go | 10 + pkg/manifest/local_server.go | 38 +++ pkg/manifest/manifest.go | 534 ----------------------------------- pkg/manifest/setup.go | 81 ++++++ pkg/manifest/version.go | 79 ++++++ 7 files changed, 553 insertions(+), 534 deletions(-) create mode 100644 pkg/manifest/data.go create mode 100644 pkg/manifest/file.go create mode 100644 pkg/manifest/flags.go create mode 100644 pkg/manifest/local_server.go create mode 100644 pkg/manifest/setup.go create mode 100644 pkg/manifest/version.go diff --git a/pkg/manifest/data.go b/pkg/manifest/data.go new file mode 100644 index 000000000..74f7dcd2a --- /dev/null +++ b/pkg/manifest/data.go @@ -0,0 +1,67 @@ +package manifest + +import ( + "os" + + "github.com/fastly/cli/pkg/env" +) + +// Data holds global-ish manifest data from manifest files, and flag sources. +// It has methods to give each parameter to the components that need it, +// including the place the parameter came from, which is a requirement. +// +// If the same parameter is defined in multiple places, it is resolved according +// to the following priority order: the manifest file (lowest priority) and then +// explicit flags (highest priority). +type Data struct { + File File + Flag Flag +} + +// Name yields a Name. +func (d *Data) Name() (string, Source) { + if d.File.Name != "" { + return d.File.Name, SourceFile + } + + return "", SourceUndefined +} + +// ServiceID yields a ServiceID. +func (d *Data) ServiceID() (string, Source) { + if d.Flag.ServiceID != "" { + return d.Flag.ServiceID, SourceFlag + } + + if sid := os.Getenv(env.ServiceID); sid != "" { + return sid, SourceEnv + } + + if d.File.ServiceID != "" { + return d.File.ServiceID, SourceFile + } + + return "", SourceUndefined +} + +// Description yields a Description. +func (d *Data) Description() (string, Source) { + if d.File.Description != "" { + return d.File.Description, SourceFile + } + + return "", SourceUndefined +} + +// Authors yields an Authors. +func (d *Data) Authors() ([]string, Source) { + if len(d.Flag.Authors) > 0 { + return d.Flag.Authors, SourceFlag + } + + if len(d.File.Authors) > 0 { + return d.File.Authors, SourceFile + } + + return []string{}, SourceUndefined +} diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go new file mode 100644 index 000000000..ac7414a20 --- /dev/null +++ b/pkg/manifest/file.go @@ -0,0 +1,278 @@ +package manifest + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/text" + toml "github.com/pelletier/go-toml" +) + +// File represents all of the configuration parameters in the fastly.toml +// manifest file schema. +type File struct { + Authors []string `toml:"authors"` + Description string `toml:"description"` + Language string `toml:"language"` + Profile string `toml:"profile,omitempty"` + LocalServer LocalServer `toml:"local_server,omitempty"` + ManifestVersion Version `toml:"manifest_version"` + Name string `toml:"name"` + Scripts Scripts `toml:"scripts,omitempty"` + ServiceID string `toml:"service_id"` + Setup Setup `toml:"setup,omitempty"` + + quiet bool + errLog fsterr.LogInterface + exists bool + output io.Writer + readError error +} + +// SetQuiet sets the associated flag value. +func (f *File) SetQuiet(v bool) { + f.quiet = v +} + +// Exists yields whether the manifest exists. +// +// Specifically, it indicates that a toml.Unmarshal() of the toml disk content +// to data in memory was successful without error. +func (f *File) Exists() bool { + return f.exists +} + +// ReadError yields the error returned from Read(). +// +// NOTE: We no longer call Read() from every command. We only call it once +// within app.Run() but we don't handle any errors that are returned from the +// Read() method. This is because failing to read the manifest is fine if the +// error is caused by the file not existing in a directory where the user is +// working on a non-C@E project. This will enable code elsewhere in the CLI to +// understand why the Read() failed. For example, we can use errors.Is() to +// allow returning a specific remediation error from a C@E related command. +func (f *File) ReadError() error { + return f.readError +} + +// SetErrLog sets an instance of errors.LogInterface. +func (f *File) SetErrLog(errLog fsterr.LogInterface) { + f.errLog = errLog +} + +// SetOutput sets the output stream for any messages. +func (f *File) SetOutput(output io.Writer) { + f.output = output +} + +func (f *File) logErr(err error) { + if f.errLog != nil { + f.errLog.Add(err) + } +} + +// AutoMigrateVersion updates the manifest_version value to +// ManifestLatestVersion if the current version is less than the latest +// supported and only if there is no [setup] or [local_server] configuration defined. +// +// NOTE: It contains similar conversions to the custom Version.UnmarshalText(). +// Specifically, it type switches the any into various types before +// attempting to convert the underlying value into an integer. +func (f *File) AutoMigrateVersion(data []byte, path string) ([]byte, error) { + tree, err := toml.LoadBytes(data) + if err != nil { + return data, err + } + + // If there is no manifest_version set then we return the fastly.toml content + // unmodified, along with a nil error, so that logic further down the .Read() + // method will pick up that the unmarshalled data structure will have a zero + // value of 0 for the ManifestVersion field and so will display a message to + // the user to inform them that we'll default to setting a manifest_version to + // the ManifestLatestVersion value. + i := tree.GetArray("manifest_version") + if i == nil { + return data, nil + } + + setup := tree.GetArray("setup") + + var version int + switch v := i.(type) { + case int64: + version = int(v) + case float64: + version = int(v) + case string: + if strings.Contains(v, ".") { + // Presumes semver value (e.g. 1.0.0, 0.1.0 or 0.1) + // Major is converted to integer if != zero. + // Otherwise if Major == zero, then ignore Minor/Patch and set to latest version. + segs := strings.Split(v, ".") + v = segs[0] + } + version, err = strconv.Atoi(v) + if err != nil { + return data, fmt.Errorf("error parsing manifest_version: %w", err) + } + default: + return data, fmt.Errorf("error parsing manifest_version: unrecognised type") + } + + // User is on the latest version supported by the CLI, so we'll return the + // []byte with the manifest_version field unmodified. + if version == ManifestLatestVersion { + return data, nil + } + + // User has an unrecognised manifest_version specified. + if version > ManifestLatestVersion { + return data, fsterr.ErrUnrecognisedManifestVersion + } + + // User has manifest_version less than latest supported by CLI, but as they + // don't have a [setup] configuration block defined, it means we can + // automatically update their fastly.toml file's manifest_version field. + // + // NOTE: Inside this block we also update the data variable so it contains the + // updated manifest_version field too, and that is returned at the end of + // the function block. + if setup == nil { + tree.Set("manifest_version", int64(ManifestLatestVersion)) + + data, err = tree.Marshal() + if err != nil { + return nil, fmt.Errorf("error marshalling modified manifest_version fastly.toml: %w", err) + } + + // NOTE: The scenario will end up triggering two calls to toml.Unmarshal(). + // The first call here, then a second call inside of the File.Read() caller. + // This only happens once. All future file reads result in one Unmarshal. + err = toml.Unmarshal(data, f) + if err != nil { + return data, fmt.Errorf("error unmarshaling fastly.toml: %w", err) + } + + if err = f.Write(path); err != nil { + return data, fsterr.ErrIncompatibleManifestVersion + } + + return data, nil + } + + return data, fsterr.ErrIncompatibleManifestVersion +} + +// Load parses the input data into the File struct and persists it to disk. +// +// NOTE: This is used by the `compute build` command logic. +// Which has to modify the toml tree for supporting a v4.0.0 migration path. +// e.g. if user manifest is missing [scripts.build] then add a default value. +func (f *File) Load(data []byte) error { + err := toml.Unmarshal(data, f) + if err != nil { + return fmt.Errorf("error unmarshaling fastly.toml: %w", err) + } + return f.Write(Filename) +} + +// Read loads the manifest file content from disk. +func (f *File) Read(path string) (err error) { + defer func() { + if err != nil { + f.readError = err + } + }() + + // gosec flagged this: + // G304 (CWE-22): Potential file inclusion via variable. + // Disabling as we need to load the fastly.toml from the user's file system. + // This file is decoded into a predefined struct, any unrecognised fields are dropped. + /* #nosec */ + data, err := os.ReadFile(path) + if err != nil { + f.logErr(err) + return err + } + + // The AutoMigrateVersion() method will either return the []byte unmodified or + // it will have updated the manifest_version field to reflect the latest + // version supported by the Fastly CLI. + data, err = f.AutoMigrateVersion(data, path) + if err != nil { + f.logErr(err) + return err + } + + err = toml.Unmarshal(data, f) + if err != nil { + f.logErr(err) + return fsterr.ErrParsingManifest + } + + if f.ManifestVersion == 0 { + f.ManifestVersion = ManifestLatestVersion + + if !f.quiet { + text.Warning(f.output, fmt.Sprintf("The fastly.toml was missing a `manifest_version` field. A default schema version of `%d` will be used.", ManifestLatestVersion)) + text.Break(f.output) + text.Output(f.output, fmt.Sprintf("Refer to the fastly.toml package manifest format: %s", SpecURL)) + text.Break(f.output) + } + err = f.Write(path) + if err != nil { + f.logErr(err) + return fmt.Errorf("unable to save fastly.toml manifest change: %w", err) + } + } + + f.exists = true + + return nil +} + +// Write persists the manifest content to disk. +func (f *File) Write(path string) error { + // gosec flagged this: + // G304 (CWE-22): Potential file inclusion via variable + // + // Disabling as in most cases this is provided by a static constant embedded + // from the 'manifest' package, and in other cases we want the user to be + // able to provide a custom path to their fastly.toml manifest. + /* #nosec */ + fp, err := os.Create(path) + if err != nil { + return err + } + + if err := appendSpecRef(fp); err != nil { + return err + } + + if err := toml.NewEncoder(fp).Encode(f); err != nil { + return err + } + + if err := fp.Sync(); err != nil { + return err + } + + return fp.Close() +} + +// appendSpecRef appends the fastly.toml specification URL to the manifest. +func appendSpecRef(w io.Writer) error { + s := fmt.Sprintf("# %s\n# %s\n\n", SpecIntro, SpecURL) + _, err := io.WriteString(w, s) + return err +} + +// Scripts represents build configuration. +type Scripts struct { + Build string `toml:"build,omitempty"` + PostBuild string `toml:"post_build,omitempty"` +} diff --git a/pkg/manifest/flags.go b/pkg/manifest/flags.go new file mode 100644 index 000000000..174c5fc7d --- /dev/null +++ b/pkg/manifest/flags.go @@ -0,0 +1,10 @@ +package manifest + +// Flag represents all of the manifest parameters that can be set with explicit +// flags. Consumers should bind their flag values to these fields directly. +type Flag struct { + Name string + Description string + Authors []string + ServiceID string +} diff --git a/pkg/manifest/local_server.go b/pkg/manifest/local_server.go new file mode 100644 index 000000000..ceecf798e --- /dev/null +++ b/pkg/manifest/local_server.go @@ -0,0 +1,38 @@ +package manifest + +// LocalServer represents a list of mocked Viceroy resources. +type LocalServer struct { + Backends map[string]LocalBackend `toml:"backends"` + Dictionaries map[string]LocalDictionary `toml:"dictionaries,omitempty"` + KVStores map[string][]LocalKVStore `toml:"kv_stores,omitempty"` + SecretStores map[string][]LocalSecretStore `toml:"secret_stores,omitempty"` +} + +// LocalBackend represents a backend to be mocked by the local testing server. +type LocalBackend struct { + URL string `toml:"url"` + OverrideHost string `toml:"override_host,omitempty"` + CertHost string `toml:"cert_host,omitempty"` + UseSNI bool `toml:"use_sni,omitempty"` +} + +// LocalDictionary represents a dictionary to be mocked by the local testing server. +type LocalDictionary struct { + File string `toml:"file,omitempty"` + Format string `toml:"format"` + Contents map[string]string `toml:"contents,omitempty"` +} + +// LocalKVStore represents an kv_store to be mocked by the local testing server. +type LocalKVStore struct { + Key string `toml:"key"` + File string `toml:"file,omitempty"` + Data string `toml:"data,omitempty"` +} + +// LocalSecretStore represents a secret_store to be mocked by the local testing server. +type LocalSecretStore struct { + Key string `toml:"key"` + File string `toml:"file,omitempty"` + Data string `toml:"data,omitempty"` +} diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index 93aee2405..552638ca2 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -1,18 +1,5 @@ package manifest -import ( - "fmt" - "io" - "os" - "strconv" - "strings" - - "github.com/fastly/cli/pkg/env" - fsterr "github.com/fastly/cli/pkg/errors" - "github.com/fastly/cli/pkg/text" - toml "github.com/pelletier/go-toml" -) - // Source enumerates where a manifest parameter is taken from. type Source uint8 @@ -50,524 +37,3 @@ const ( // SpecURL points to the fastly.toml manifest specification reference. SpecURL = "https://developer.fastly.com/reference/fastly-toml/" ) - -// Data holds global-ish manifest data from manifest files, and flag sources. -// It has methods to give each parameter to the components that need it, -// including the place the parameter came from, which is a requirement. -// -// If the same parameter is defined in multiple places, it is resolved according -// to the following priority order: the manifest file (lowest priority) and then -// explicit flags (highest priority). -type Data struct { - File File - Flag Flag -} - -// Name yields a Name. -func (d *Data) Name() (string, Source) { - if d.File.Name != "" { - return d.File.Name, SourceFile - } - - return "", SourceUndefined -} - -// ServiceID yields a ServiceID. -func (d *Data) ServiceID() (string, Source) { - if d.Flag.ServiceID != "" { - return d.Flag.ServiceID, SourceFlag - } - - if sid := os.Getenv(env.ServiceID); sid != "" { - return sid, SourceEnv - } - - if d.File.ServiceID != "" { - return d.File.ServiceID, SourceFile - } - - return "", SourceUndefined -} - -// Description yields a Description. -func (d *Data) Description() (string, Source) { - if d.File.Description != "" { - return d.File.Description, SourceFile - } - - return "", SourceUndefined -} - -// Authors yields an Authors. -func (d *Data) Authors() ([]string, Source) { - if len(d.Flag.Authors) > 0 { - return d.Flag.Authors, SourceFlag - } - - if len(d.File.Authors) > 0 { - return d.File.Authors, SourceFile - } - - return []string{}, SourceUndefined -} - -// Version represents the currently supported schema for the fastly.toml -// manifest file that determines the configuration for a compute@edge service. -// -// NOTE: the File object has a field called ManifestVersion which this type is -// assigned. The reason we don't name this type ManifestVersion is to appease -// the static analysis linter which complains re: stutter in the import -// manifest.ManifestVersion. -type Version int - -// UnmarshalText manages multiple scenarios where historically the manifest -// version was a string value and not an integer. -// -// Example mappings: -// -// "0.1.0" -> 1 -// "1" -> 1 -// 1 -> 1 -// "1.0.0" -> 1 -// 0.1 -> 1 -// "0.2.0" -> 1 -// "2.0.0" -> 2 -// -// We also constrain the version so that if a user has a manifest_version -// defined as "99.0.0" then we won't accidentally store it as the integer 99 -// but instead will return an error because it exceeds the current -// ManifestLatestVersion version. -func (v *Version) UnmarshalText(txt []byte) error { - s := string(txt) - - if i, err := strconv.Atoi(s); err == nil { - *v = Version(i) - return nil - } - - if f, err := strconv.ParseFloat(s, 32); err == nil { - intfl := int(f) - if intfl == 0 { - *v = ManifestLatestVersion - } else { - *v = Version(intfl) - } - return nil - } - - // Presumes semver value (e.g. 1.0.0, 0.1.0 or 0.1) - // Major is converted to integer if != zero. - // Otherwise if Major == zero, then ignore Minor/Patch and set to latest version. - var ( - err error - version int - ) - if strings.Contains(s, ".") { - segs := strings.Split(s, ".") - s = segs[0] - if s == "0" { - s = strconv.Itoa(ManifestLatestVersion) - } - } - version, err = strconv.Atoi(s) - if err != nil { - return fmt.Errorf("error parsing manifest_version: %w", err) - } - - if version > ManifestLatestVersion { - return fsterr.ErrUnrecognisedManifestVersion - } - *v = Version(version) - return nil -} - -// File represents all of the configuration parameters in the fastly.toml -// manifest file schema. -type File struct { - Authors []string `toml:"authors"` - Description string `toml:"description"` - Language string `toml:"language"` - Profile string `toml:"profile,omitempty"` - LocalServer LocalServer `toml:"local_server,omitempty"` - ManifestVersion Version `toml:"manifest_version"` - Name string `toml:"name"` - Scripts Scripts `toml:"scripts,omitempty"` - ServiceID string `toml:"service_id"` - Setup Setup `toml:"setup,omitempty"` - - quiet bool - errLog fsterr.LogInterface - exists bool - output io.Writer - readError error -} - -// SetQuiet sets the associated flag value. -func (f *File) SetQuiet(v bool) { - f.quiet = v -} - -// Scripts represents build configuration. -type Scripts struct { - Build string `toml:"build,omitempty"` - PostBuild string `toml:"post_build,omitempty"` -} - -// Setup represents a set of service configuration that works with the code in -// the package. See https://developer.fastly.com/reference/fastly-toml/. -type Setup struct { - Backends map[string]*SetupBackend `toml:"backends,omitempty"` - ConfigStores map[string]*SetupConfigStore `toml:"config_stores,omitempty"` - Loggers map[string]*SetupLogger `toml:"log_endpoints,omitempty"` - KVStores map[string]*SetupKVStore `toml:"kv_stores,omitempty"` - SecretStores map[string]*SetupSecretStore `toml:"secret_stores,omitempty"` -} - -// Defined indicates if there is any [setup] configuration in the manifest. -func (s Setup) Defined() bool { - var defined bool - - if len(s.Backends) > 0 { - defined = true - } - if len(s.ConfigStores) > 0 { - defined = true - } - if len(s.Loggers) > 0 { - defined = true - } - if len(s.KVStores) > 0 { - defined = true - } - - return defined -} - -// SetupBackend represents a '[setup.backends.]' instance. -type SetupBackend struct { - Address string `toml:"address,omitempty"` - Port int `toml:"port,omitempty"` - Description string `toml:"description,omitempty"` -} - -// SetupConfigStore represents a '[setup.dictionaries.]' instance. -type SetupConfigStore struct { - Items map[string]SetupConfigStoreItems `toml:"items,omitempty"` - Description string `toml:"description,omitempty"` -} - -// SetupConfigStoreItems represents a '[setup.dictionaries..items]' instance. -type SetupConfigStoreItems struct { - Value string `toml:"value,omitempty"` - Description string `toml:"description,omitempty"` -} - -// SetupLogger represents a '[setup.log_endpoints.]' instance. -type SetupLogger struct { - Provider string `toml:"provider,omitempty"` -} - -// SetupKVStore represents a '[setup.kv_stores.]' instance. -type SetupKVStore struct { - Items map[string]SetupKVStoreItems `toml:"items,omitempty"` - Description string `toml:"description,omitempty"` -} - -// SetupKVStoreItems represents a '[setup.kv_stores..items]' instance. -type SetupKVStoreItems struct { - Value string `toml:"value,omitempty"` - Description string `toml:"description,omitempty"` -} - -// SetupSecretStore represents a '[setup.secret_stores.]' instance. -type SetupSecretStore struct { - Entries map[string]SetupSecretStoreEntry `toml:"entries,omitempty"` - Description string `toml:"description,omitempty"` -} - -// SetupSecretStoreEntry represents a '[setup.secret_stores..entries]' instance. -type SetupSecretStoreEntry struct { - // The secret value is intentionally omitted to avoid secrets - // from being included in the manifest. Instead, secret - // values are input during setup. - Description string `toml:"description,omitempty"` -} - -// LocalServer represents a list of mocked Viceroy resources. -type LocalServer struct { - Backends map[string]LocalBackend `toml:"backends"` - Dictionaries map[string]LocalDictionary `toml:"dictionaries,omitempty"` - KVStores map[string][]LocalKVStore `toml:"kv_stores,omitempty"` - SecretStores map[string][]LocalSecretStore `toml:"secret_stores,omitempty"` -} - -// LocalBackend represents a backend to be mocked by the local testing server. -type LocalBackend struct { - URL string `toml:"url"` - OverrideHost string `toml:"override_host,omitempty"` - CertHost string `toml:"cert_host,omitempty"` - UseSNI bool `toml:"use_sni,omitempty"` -} - -// LocalDictionary represents a dictionary to be mocked by the local testing server. -type LocalDictionary struct { - File string `toml:"file,omitempty"` - Format string `toml:"format"` - Contents map[string]string `toml:"contents,omitempty"` -} - -// LocalKVStore represents an kv_store to be mocked by the local testing server. -type LocalKVStore struct { - Key string `toml:"key"` - File string `toml:"file,omitempty"` - Data string `toml:"data,omitempty"` -} - -// LocalSecretStore represents a secret_store to be mocked by the local testing server. -type LocalSecretStore struct { - Key string `toml:"key"` - File string `toml:"file,omitempty"` - Data string `toml:"data,omitempty"` -} - -// Exists yields whether the manifest exists. -// -// Specifically, it indicates that a toml.Unmarshal() of the toml disk content -// to data in memory was successful without error. -func (f *File) Exists() bool { - return f.exists -} - -// ReadError yields the error returned from Read(). -// -// NOTE: We no longer call Read() from every command. We only call it once -// within app.Run() but we don't handle any errors that are returned from the -// Read() method. This is because failing to read the manifest is fine if the -// error is caused by the file not existing in a directory where the user is -// working on a non-C@E project. This will enable code elsewhere in the CLI to -// understand why the Read() failed. For example, we can use errors.Is() to -// allow returning a specific remediation error from a C@E related command. -func (f *File) ReadError() error { - return f.readError -} - -// SetErrLog sets an instance of errors.LogInterface. -func (f *File) SetErrLog(errLog fsterr.LogInterface) { - f.errLog = errLog -} - -// SetOutput sets the output stream for any messages. -func (f *File) SetOutput(output io.Writer) { - f.output = output -} - -func (f *File) logErr(err error) { - if f.errLog != nil { - f.errLog.Add(err) - } -} - -// AutoMigrateVersion updates the manifest_version value to -// ManifestLatestVersion if the current version is less than the latest -// supported and only if there is no [setup] configuration defined. -// -// NOTE: It contains similar conversions to the custom Version.UnmarshalText(). -// Specifically, it type switches the any into various types before -// attempting to convert the underlying value into an integer. -func (f *File) AutoMigrateVersion(data []byte, path string) ([]byte, error) { - tree, err := toml.LoadBytes(data) - if err != nil { - return data, err - } - - // If there is no manifest_version set then we return the fastly.toml content - // unmodified, along with a nil error, so that logic further down the .Read() - // method will pick up that the unmarshalled data structure will have a zero - // value of 0 for the ManifestVersion field and so will display a message to - // the user to inform them that we'll default to setting a manifest_version to - // the ManifestLatestVersion value. - i := tree.GetArray("manifest_version") - if i == nil { - return data, nil - } - - setup := tree.GetArray("setup") - - var version int - switch v := i.(type) { - case int64: - version = int(v) - case float64: - version = int(v) - case string: - if strings.Contains(v, ".") { - // Presumes semver value (e.g. 1.0.0, 0.1.0 or 0.1) - // Major is converted to integer if != zero. - // Otherwise if Major == zero, then ignore Minor/Patch and set to latest version. - segs := strings.Split(v, ".") - v = segs[0] - } - version, err = strconv.Atoi(v) - if err != nil { - return data, fmt.Errorf("error parsing manifest_version: %w", err) - } - default: - return data, fmt.Errorf("error parsing manifest_version: unrecognised type") - } - - // User is on the latest version supported by the CLI, so we'll return the - // []byte with the manifest_version field unmodified. - if version == ManifestLatestVersion { - return data, nil - } - - // User has an unrecognised manifest_version specified. - if version > ManifestLatestVersion { - return data, fsterr.ErrUnrecognisedManifestVersion - } - - // User has manifest_version less than latest supported by CLI, but as they - // don't have a [setup] configuration block defined, it means we can - // automatically update their fastly.toml file's manifest_version field. - // - // NOTE: Inside this block we also update the data variable so it contains the - // updated manifest_version field too, and that is returned at the end of - // the function block. - if setup == nil { - tree.Set("manifest_version", int64(ManifestLatestVersion)) - - data, err = tree.Marshal() - if err != nil { - return nil, fmt.Errorf("error marshalling modified manifest_version fastly.toml: %w", err) - } - - // NOTE: The scenario will end up triggering two calls to toml.Unmarshal(). - // The first call here, then a second call inside of the File.Read() caller. - // This only happens once. All future file reads result in one Unmarshal. - err = toml.Unmarshal(data, f) - if err != nil { - return data, fmt.Errorf("error unmarshaling fastly.toml: %w", err) - } - - if err = f.Write(path); err != nil { - return data, fsterr.ErrIncompatibleManifestVersion - } - - return data, nil - } - - return data, fsterr.ErrIncompatibleManifestVersion -} - -// Load parses the input data into the File struct and persists it to disk. -// -// NOTE: This is used by the `compute build` command logic. -// Which has to modify the toml tree for supporting a v4.0.0 migration path. -// e.g. if user manifest is missing [scripts.build] then add a default value. -func (f *File) Load(data []byte) error { - err := toml.Unmarshal(data, f) - if err != nil { - return fmt.Errorf("error unmarshaling fastly.toml: %w", err) - } - return f.Write(Filename) -} - -// Read loads the manifest file content from disk. -func (f *File) Read(path string) (err error) { - defer func() { - if err != nil { - f.readError = err - } - }() - - // gosec flagged this: - // G304 (CWE-22): Potential file inclusion via variable. - // Disabling as we need to load the fastly.toml from the user's file system. - // This file is decoded into a predefined struct, any unrecognised fields are dropped. - /* #nosec */ - data, err := os.ReadFile(path) - if err != nil { - f.logErr(err) - return err - } - - // The AutoMigrateVersion() method will either return the []byte unmodified or - // it will have updated the manifest_version field to reflect the latest - // version supported by the Fastly CLI. - data, err = f.AutoMigrateVersion(data, path) - if err != nil { - f.logErr(err) - return err - } - - err = toml.Unmarshal(data, f) - if err != nil { - f.logErr(err) - return fsterr.ErrParsingManifest - } - - if f.ManifestVersion == 0 { - f.ManifestVersion = ManifestLatestVersion - - if !f.quiet { - text.Warning(f.output, fmt.Sprintf("The fastly.toml was missing a `manifest_version` field. A default schema version of `%d` will be used.", ManifestLatestVersion)) - text.Break(f.output) - text.Output(f.output, fmt.Sprintf("Refer to the fastly.toml package manifest format: %s", SpecURL)) - text.Break(f.output) - } - err = f.Write(path) - if err != nil { - f.logErr(err) - return fmt.Errorf("unable to save fastly.toml manifest change: %w", err) - } - } - - f.exists = true - - return nil -} - -// Write persists the manifest content to disk. -func (f *File) Write(path string) error { - // gosec flagged this: - // G304 (CWE-22): Potential file inclusion via variable - // - // Disabling as in most cases this is provided by a static constant embedded - // from the 'manifest' package, and in other cases we want the user to be - // able to provide a custom path to their fastly.toml manifest. - /* #nosec */ - fp, err := os.Create(path) - if err != nil { - return err - } - - if err := appendSpecRef(fp); err != nil { - return err - } - - if err := toml.NewEncoder(fp).Encode(f); err != nil { - return err - } - - if err := fp.Sync(); err != nil { - return err - } - - return fp.Close() -} - -// appendSpecRef appends the fastly.toml specification URL to the manifest. -func appendSpecRef(w io.Writer) error { - s := fmt.Sprintf("# %s\n# %s\n\n", SpecIntro, SpecURL) - _, err := io.WriteString(w, s) - return err -} - -// Flag represents all of the manifest parameters that can be set with explicit -// flags. Consumers should bind their flag values to these fields directly. -type Flag struct { - Name string - Description string - Authors []string - ServiceID string -} diff --git a/pkg/manifest/setup.go b/pkg/manifest/setup.go new file mode 100644 index 000000000..271cef942 --- /dev/null +++ b/pkg/manifest/setup.go @@ -0,0 +1,81 @@ +package manifest + +// Setup represents a set of service configuration that works with the code in +// the package. See https://developer.fastly.com/reference/fastly-toml/. +type Setup struct { + Backends map[string]*SetupBackend `toml:"backends,omitempty"` + ConfigStores map[string]*SetupConfigStore `toml:"config_stores,omitempty"` + Loggers map[string]*SetupLogger `toml:"log_endpoints,omitempty"` + KVStores map[string]*SetupKVStore `toml:"kv_stores,omitempty"` + SecretStores map[string]*SetupSecretStore `toml:"secret_stores,omitempty"` +} + +// Defined indicates if there is any [setup] configuration in the manifest. +func (s Setup) Defined() bool { + var defined bool + + if len(s.Backends) > 0 { + defined = true + } + if len(s.ConfigStores) > 0 { + defined = true + } + if len(s.Loggers) > 0 { + defined = true + } + if len(s.KVStores) > 0 { + defined = true + } + + return defined +} + +// SetupBackend represents a '[setup.backends.]' instance. +type SetupBackend struct { + Address string `toml:"address,omitempty"` + Port int `toml:"port,omitempty"` + Description string `toml:"description,omitempty"` +} + +// SetupConfigStore represents a '[setup.dictionaries.]' instance. +type SetupConfigStore struct { + Items map[string]SetupConfigStoreItems `toml:"items,omitempty"` + Description string `toml:"description,omitempty"` +} + +// SetupConfigStoreItems represents a '[setup.dictionaries..items]' instance. +type SetupConfigStoreItems struct { + Value string `toml:"value,omitempty"` + Description string `toml:"description,omitempty"` +} + +// SetupLogger represents a '[setup.log_endpoints.]' instance. +type SetupLogger struct { + Provider string `toml:"provider,omitempty"` +} + +// SetupKVStore represents a '[setup.kv_stores.]' instance. +type SetupKVStore struct { + Items map[string]SetupKVStoreItems `toml:"items,omitempty"` + Description string `toml:"description,omitempty"` +} + +// SetupKVStoreItems represents a '[setup.kv_stores..items]' instance. +type SetupKVStoreItems struct { + Value string `toml:"value,omitempty"` + Description string `toml:"description,omitempty"` +} + +// SetupSecretStore represents a '[setup.secret_stores.]' instance. +type SetupSecretStore struct { + Entries map[string]SetupSecretStoreEntry `toml:"entries,omitempty"` + Description string `toml:"description,omitempty"` +} + +// SetupSecretStoreEntry represents a '[setup.secret_stores..entries]' instance. +type SetupSecretStoreEntry struct { + // The secret value is intentionally omitted to avoid secrets + // from being included in the manifest. Instead, secret + // values are input during setup. + Description string `toml:"description,omitempty"` +} diff --git a/pkg/manifest/version.go b/pkg/manifest/version.go new file mode 100644 index 000000000..812641e06 --- /dev/null +++ b/pkg/manifest/version.go @@ -0,0 +1,79 @@ +package manifest + +import ( + "fmt" + "strconv" + "strings" + + fsterr "github.com/fastly/cli/pkg/errors" +) + +// Version represents the currently supported schema for the fastly.toml +// manifest file that determines the configuration for a compute@edge service. +// +// NOTE: the File object has a field called ManifestVersion which this type is +// assigned. The reason we don't name this type ManifestVersion is to appease +// the static analysis linter which complains re: stutter in the import +// manifest.ManifestVersion. +type Version int + +// UnmarshalText manages multiple scenarios where historically the manifest +// version was a string value and not an integer. +// +// Example mappings: +// +// "0.1.0" -> 1 +// "1" -> 1 +// 1 -> 1 +// "1.0.0" -> 1 +// 0.1 -> 1 +// "0.2.0" -> 1 +// "2.0.0" -> 2 +// +// We also constrain the version so that if a user has a manifest_version +// defined as "99.0.0" then we won't accidentally store it as the integer 99 +// but instead will return an error because it exceeds the current +// ManifestLatestVersion version. +func (v *Version) UnmarshalText(txt []byte) error { + s := string(txt) + + if i, err := strconv.Atoi(s); err == nil { + *v = Version(i) + return nil + } + + if f, err := strconv.ParseFloat(s, 32); err == nil { + intfl := int(f) + if intfl == 0 { + *v = ManifestLatestVersion + } else { + *v = Version(intfl) + } + return nil + } + + // Presumes semver value (e.g. 1.0.0, 0.1.0 or 0.1) + // Major is converted to integer if != zero. + // Otherwise if Major == zero, then ignore Minor/Patch and set to latest version. + var ( + err error + version int + ) + if strings.Contains(s, ".") { + segs := strings.Split(s, ".") + s = segs[0] + if s == "0" { + s = strconv.Itoa(ManifestLatestVersion) + } + } + version, err = strconv.Atoi(s) + if err != nil { + return fmt.Errorf("error parsing manifest_version: %w", err) + } + + if version > ManifestLatestVersion { + return fsterr.ErrUnrecognisedManifestVersion + } + *v = Version(version) + return nil +} From 535b990803f45d3a28eae74b20183c888f1cefa5 Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 15:00:39 +0100 Subject: [PATCH 05/14] refactor(manifest): reorder AST --- pkg/manifest/file.go | 84 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index ac7414a20..7a0b87287 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -33,48 +33,6 @@ type File struct { readError error } -// SetQuiet sets the associated flag value. -func (f *File) SetQuiet(v bool) { - f.quiet = v -} - -// Exists yields whether the manifest exists. -// -// Specifically, it indicates that a toml.Unmarshal() of the toml disk content -// to data in memory was successful without error. -func (f *File) Exists() bool { - return f.exists -} - -// ReadError yields the error returned from Read(). -// -// NOTE: We no longer call Read() from every command. We only call it once -// within app.Run() but we don't handle any errors that are returned from the -// Read() method. This is because failing to read the manifest is fine if the -// error is caused by the file not existing in a directory where the user is -// working on a non-C@E project. This will enable code elsewhere in the CLI to -// understand why the Read() failed. For example, we can use errors.Is() to -// allow returning a specific remediation error from a C@E related command. -func (f *File) ReadError() error { - return f.readError -} - -// SetErrLog sets an instance of errors.LogInterface. -func (f *File) SetErrLog(errLog fsterr.LogInterface) { - f.errLog = errLog -} - -// SetOutput sets the output stream for any messages. -func (f *File) SetOutput(output io.Writer) { - f.output = output -} - -func (f *File) logErr(err error) { - if f.errLog != nil { - f.errLog.Add(err) - } -} - // AutoMigrateVersion updates the manifest_version value to // ManifestLatestVersion if the current version is less than the latest // supported and only if there is no [setup] or [local_server] configuration defined. @@ -167,6 +125,14 @@ func (f *File) AutoMigrateVersion(data []byte, path string) ([]byte, error) { return data, fsterr.ErrIncompatibleManifestVersion } +// Exists yields whether the manifest exists. +// +// Specifically, it indicates that a toml.Unmarshal() of the toml disk content +// to data in memory was successful without error. +func (f *File) Exists() bool { + return f.exists +} + // Load parses the input data into the File struct and persists it to disk. // // NOTE: This is used by the `compute build` command logic. @@ -235,6 +201,34 @@ func (f *File) Read(path string) (err error) { return nil } +// ReadError yields the error returned from Read(). +// +// NOTE: We no longer call Read() from every command. We only call it once +// within app.Run() but we don't handle any errors that are returned from the +// Read() method. This is because failing to read the manifest is fine if the +// error is caused by the file not existing in a directory where the user is +// working on a non-C@E project. This will enable code elsewhere in the CLI to +// understand why the Read() failed. For example, we can use errors.Is() to +// allow returning a specific remediation error from a C@E related command. +func (f *File) ReadError() error { + return f.readError +} + +// SetErrLog sets an instance of errors.LogInterface. +func (f *File) SetErrLog(errLog fsterr.LogInterface) { + f.errLog = errLog +} + +// SetOutput sets the output stream for any messages. +func (f *File) SetOutput(output io.Writer) { + f.output = output +} + +// SetQuiet sets the associated flag value. +func (f *File) SetQuiet(v bool) { + f.quiet = v +} + // Write persists the manifest content to disk. func (f *File) Write(path string) error { // gosec flagged this: @@ -264,6 +258,12 @@ func (f *File) Write(path string) error { return fp.Close() } +func (f *File) logErr(err error) { + if f.errLog != nil { + f.errLog.Add(err) + } +} + // appendSpecRef appends the fastly.toml specification URL to the manifest. func appendSpecRef(w io.Writer) error { s := fmt.Sprintf("# %s\n# %s\n\n", SpecIntro, SpecURL) From b4d5d44ec4d37b18b05bfbbdada5aff122f37def Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 15:03:13 +0100 Subject: [PATCH 06/14] refactor(manifest/data): reorder AST --- pkg/manifest/data.go | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/manifest/data.go b/pkg/manifest/data.go index 74f7dcd2a..b7e2dfdcc 100644 --- a/pkg/manifest/data.go +++ b/pkg/manifest/data.go @@ -18,6 +18,28 @@ type Data struct { Flag Flag } +// Authors yields an Authors. +func (d *Data) Authors() ([]string, Source) { + if len(d.Flag.Authors) > 0 { + return d.Flag.Authors, SourceFlag + } + + if len(d.File.Authors) > 0 { + return d.File.Authors, SourceFile + } + + return []string{}, SourceUndefined +} + +// Description yields a Description. +func (d *Data) Description() (string, Source) { + if d.File.Description != "" { + return d.File.Description, SourceFile + } + + return "", SourceUndefined +} + // Name yields a Name. func (d *Data) Name() (string, Source) { if d.File.Name != "" { @@ -43,25 +65,3 @@ func (d *Data) ServiceID() (string, Source) { return "", SourceUndefined } - -// Description yields a Description. -func (d *Data) Description() (string, Source) { - if d.File.Description != "" { - return d.File.Description, SourceFile - } - - return "", SourceUndefined -} - -// Authors yields an Authors. -func (d *Data) Authors() ([]string, Source) { - if len(d.Flag.Authors) > 0 { - return d.Flag.Authors, SourceFlag - } - - if len(d.File.Authors) > 0 { - return d.File.Authors, SourceFile - } - - return []string{}, SourceUndefined -} From 22b4e92153896a6462637932dea5aacccb574046 Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 15:11:39 +0100 Subject: [PATCH 07/14] refactor(manifest/local_server): rename dictionaries to config_stores --- pkg/manifest/local_server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/manifest/local_server.go b/pkg/manifest/local_server.go index ceecf798e..6b1e7a40b 100644 --- a/pkg/manifest/local_server.go +++ b/pkg/manifest/local_server.go @@ -3,7 +3,7 @@ package manifest // LocalServer represents a list of mocked Viceroy resources. type LocalServer struct { Backends map[string]LocalBackend `toml:"backends"` - Dictionaries map[string]LocalDictionary `toml:"dictionaries,omitempty"` + ConfigStores map[string]LocalConfigStore `toml:"config_stores,omitempty"` KVStores map[string][]LocalKVStore `toml:"kv_stores,omitempty"` SecretStores map[string][]LocalSecretStore `toml:"secret_stores,omitempty"` } @@ -16,8 +16,8 @@ type LocalBackend struct { UseSNI bool `toml:"use_sni,omitempty"` } -// LocalDictionary represents a dictionary to be mocked by the local testing server. -type LocalDictionary struct { +// LocalConfigStore represents a config store to be mocked by the local testing server. +type LocalConfigStore struct { File string `toml:"file,omitempty"` Format string `toml:"format"` Contents map[string]string `toml:"contents,omitempty"` From cb7ba135a55345d8fb00d5c6248483802e474e18 Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 16:19:43 +0100 Subject: [PATCH 08/14] refactor(manifest): remove auto-update of manifest_version --- pkg/errors/errors.go | 7 -- pkg/manifest/file.go | 109 +----------------- pkg/manifest/manifest_test.go | 45 ++++---- .../testdata/fastly-invalid-unrecognised.toml | 3 +- .../testdata/fastly-viceroy-update.toml | 82 +++++++------ pkg/manifest/version.go | 2 +- 6 files changed, 72 insertions(+), 176 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 11c3f8b0d..3cd0167d6 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -57,13 +57,6 @@ var ErrUnrecognisedManifestVersion = RemediationError{ Remediation: CLIUpdateRemediation, } -// ErrInvalidManifestVersion means the manifest_version is defined as a toml -// section. -var ErrInvalidManifestVersion = RemediationError{ - Inner: fmt.Errorf("failed to parse fastly.toml when checking if manifest_version was valid"), - Remediation: "Delete `[manifest_version]` from the fastly.toml if present", -} - // ErrIncompatibleManifestVersion means the manifest_version defined is no // longer compatible with the current CLI version. var ErrIncompatibleManifestVersion = RemediationError{ diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index 7a0b87287..8cd5426bb 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -4,8 +4,6 @@ import ( "fmt" "io" "os" - "strconv" - "strings" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/text" @@ -33,98 +31,6 @@ type File struct { readError error } -// AutoMigrateVersion updates the manifest_version value to -// ManifestLatestVersion if the current version is less than the latest -// supported and only if there is no [setup] or [local_server] configuration defined. -// -// NOTE: It contains similar conversions to the custom Version.UnmarshalText(). -// Specifically, it type switches the any into various types before -// attempting to convert the underlying value into an integer. -func (f *File) AutoMigrateVersion(data []byte, path string) ([]byte, error) { - tree, err := toml.LoadBytes(data) - if err != nil { - return data, err - } - - // If there is no manifest_version set then we return the fastly.toml content - // unmodified, along with a nil error, so that logic further down the .Read() - // method will pick up that the unmarshalled data structure will have a zero - // value of 0 for the ManifestVersion field and so will display a message to - // the user to inform them that we'll default to setting a manifest_version to - // the ManifestLatestVersion value. - i := tree.GetArray("manifest_version") - if i == nil { - return data, nil - } - - setup := tree.GetArray("setup") - - var version int - switch v := i.(type) { - case int64: - version = int(v) - case float64: - version = int(v) - case string: - if strings.Contains(v, ".") { - // Presumes semver value (e.g. 1.0.0, 0.1.0 or 0.1) - // Major is converted to integer if != zero. - // Otherwise if Major == zero, then ignore Minor/Patch and set to latest version. - segs := strings.Split(v, ".") - v = segs[0] - } - version, err = strconv.Atoi(v) - if err != nil { - return data, fmt.Errorf("error parsing manifest_version: %w", err) - } - default: - return data, fmt.Errorf("error parsing manifest_version: unrecognised type") - } - - // User is on the latest version supported by the CLI, so we'll return the - // []byte with the manifest_version field unmodified. - if version == ManifestLatestVersion { - return data, nil - } - - // User has an unrecognised manifest_version specified. - if version > ManifestLatestVersion { - return data, fsterr.ErrUnrecognisedManifestVersion - } - - // User has manifest_version less than latest supported by CLI, but as they - // don't have a [setup] configuration block defined, it means we can - // automatically update their fastly.toml file's manifest_version field. - // - // NOTE: Inside this block we also update the data variable so it contains the - // updated manifest_version field too, and that is returned at the end of - // the function block. - if setup == nil { - tree.Set("manifest_version", int64(ManifestLatestVersion)) - - data, err = tree.Marshal() - if err != nil { - return nil, fmt.Errorf("error marshalling modified manifest_version fastly.toml: %w", err) - } - - // NOTE: The scenario will end up triggering two calls to toml.Unmarshal(). - // The first call here, then a second call inside of the File.Read() caller. - // This only happens once. All future file reads result in one Unmarshal. - err = toml.Unmarshal(data, f) - if err != nil { - return data, fmt.Errorf("error unmarshaling fastly.toml: %w", err) - } - - if err = f.Write(path); err != nil { - return data, fsterr.ErrIncompatibleManifestVersion - } - - return data, nil - } - - return data, fsterr.ErrIncompatibleManifestVersion -} - // Exists yields whether the manifest exists. // // Specifically, it indicates that a toml.Unmarshal() of the toml disk content @@ -165,19 +71,10 @@ func (f *File) Read(path string) (err error) { return err } - // The AutoMigrateVersion() method will either return the []byte unmodified or - // it will have updated the manifest_version field to reflect the latest - // version supported by the Fastly CLI. - data, err = f.AutoMigrateVersion(data, path) - if err != nil { - f.logErr(err) - return err - } - err = toml.Unmarshal(data, f) if err != nil { f.logErr(err) - return fsterr.ErrParsingManifest + return err } if f.ManifestVersion == 0 { @@ -196,6 +93,10 @@ func (f *File) Read(path string) (err error) { } } + if f.ManifestVersion < ManifestLatestVersion { + return fsterr.ErrIncompatibleManifestVersion + } + f.exists = true return nil diff --git a/pkg/manifest/manifest_test.go b/pkg/manifest/manifest_test.go index b5b0b0ca8..d94adec14 100644 --- a/pkg/manifest/manifest_test.go +++ b/pkg/manifest/manifest_test.go @@ -1,10 +1,9 @@ package manifest_test import ( - "errors" + "fmt" "os" "path/filepath" - "strconv" "strings" "testing" @@ -38,13 +37,12 @@ func TestManifest(t *testing.T) { "invalid: manifest_version Atoi error": { manifest: "fastly-invalid-unrecognised.toml", valid: false, - expectedError: strconv.ErrSyntax, + expectedError: fmt.Errorf("error parsing manifest_version 'abc'"), }, "unrecognised: manifest_version exceeded limit": { - manifest: "fastly-invalid-version-exceeded.toml", - valid: false, - expectedError: fsterr.ErrUnrecognisedManifestVersion, - wantRemediationError: fsterr.ErrUnrecognisedManifestVersion.Remediation, + manifest: "fastly-invalid-version-exceeded.toml", + valid: false, + expectedError: fsterr.ErrUnrecognisedManifestVersion, }, } @@ -92,24 +90,21 @@ func TestManifest(t *testing.T) { } err = m.Read(path) - if tc.valid { - // if we expect the manifest to be valid and we get an error, then - // that's unexpected behaviour. - if err != nil { - t.Fatal(err) - } - - if m.ManifestVersion != manifest.ManifestLatestVersion { - t.Fatalf("manifest_version '%d' doesn't match latest '%d'", m.ManifestVersion, manifest.ManifestLatestVersion) - } - } else { - // otherwise if we expect the manifest to be invalid/unrecognised then - // the error should match our expectations. - if !errors.Is(err, tc.expectedError) { - t.Fatalf("incorrect error type: %T, expected: %T", err, tc.expectedError) - } - // Ensure the remediation error is as expected. - testutil.AssertRemediationErrorContains(t, err, tc.wantRemediationError) + + // If we expect an invalid config, then assert we get the right error. + if !tc.valid { + testutil.AssertErrorContains(t, err, tc.expectedError.Error()) + return + } + + // Otherwise, if we expect the manifest to be valid and we get an error, + // then that's unexpected behaviour. + if err != nil { + t.Fatal(err) + } + + if m.ManifestVersion != manifest.ManifestLatestVersion { + t.Fatalf("manifest_version '%d' doesn't match latest '%d'", m.ManifestVersion, manifest.ManifestLatestVersion) } }) } diff --git a/pkg/manifest/testdata/fastly-invalid-unrecognised.toml b/pkg/manifest/testdata/fastly-invalid-unrecognised.toml index 545b3a8ac..bdd31f99f 100644 --- a/pkg/manifest/testdata/fastly-invalid-unrecognised.toml +++ b/pkg/manifest/testdata/fastly-invalid-unrecognised.toml @@ -1,4 +1,5 @@ -manifest_version = "abc" # not a number +# invalid: manifest_version is not a number +manifest_version = "abc" name = "Default Rust template" description = "Default package template for Rust based edge compute projects." authors = ["phamann "] diff --git a/pkg/manifest/testdata/fastly-viceroy-update.toml b/pkg/manifest/testdata/fastly-viceroy-update.toml index 91fa08937..60bf2d54e 100644 --- a/pkg/manifest/testdata/fastly-viceroy-update.toml +++ b/pkg/manifest/testdata/fastly-viceroy-update.toml @@ -9,50 +9,56 @@ name = "Default Rust template" [local_server] - [local_server.backends] +[local_server.backends] - [local_server.backends.backend_a] - url = "https://example.com/" - override_host = "otherexample.com" +[local_server.backends.backend_a] +url = "https://example.com/" +override_host = "otherexample.com" - [local_server.backends.foo] - url = "https://foo.com/" +[local_server.backends.foo] +url = "https://foo.com/" - [local_server.backends.bar] - url = "https://bar.com/" +[local_server.backends.bar] +url = "https://bar.com/" - [local_server.dictionaries] +[local_server.config_stores] - [local_server.dictionaries.strings] - file = "strings.json" - format = "json" +[local_server.config_stores.strings] +file = "strings.json" +format = "json" - [local_server.dictionaries.toml] - format = "inline-toml" +[local_server.config_stores.example_store] +format = "inline-toml" - [local_server.dictionaries.toml.contents] - foo = "bar" - baz = """ +[local_server.config_stores.example_store.contents] +foo = "bar" +baz = """ qux""" - [local_server.kv_stores] - store_one = [{key = "first", data = "This is some data"}, {key = "second", file = "strings.json"}] - - [[local_server.kv_stores.store_two]] - key = "first" - data = "This is some data" - - [[local_server.kv_stores.store_two]] - key = "second" - file = "strings.json" - - [local_server.secret_stores] - store_one = [{key = "first", data = "This is some secret data"}, {key = "second", file = "/path/to/secret.json"}] - - [[local_server.secret_stores.store_two]] - key = "first" - data = "This is also some secret data" - - [[local_server.secret_stores.store_two]] - key = "second" - file = "/path/to/other/secret.json" +[local_server.kv_stores] +store_one = [ + { key = "first", data = "This is some data" }, + { key = "second", file = "strings.json" }, +] + +[[local_server.kv_stores.store_two]] +key = "first" +data = "This is some data" + +[[local_server.kv_stores.store_two]] +key = "second" +file = "strings.json" + +[local_server.secret_stores] +store_one = [ + { key = "first", data = "This is some secret data" }, + { key = "second", file = "/path/to/secret.json" }, +] + +[[local_server.secret_stores.store_two]] +key = "first" +data = "This is also some secret data" + +[[local_server.secret_stores.store_two]] +key = "second" +file = "/path/to/other/secret.json" diff --git a/pkg/manifest/version.go b/pkg/manifest/version.go index 812641e06..57e8b6375 100644 --- a/pkg/manifest/version.go +++ b/pkg/manifest/version.go @@ -68,7 +68,7 @@ func (v *Version) UnmarshalText(txt []byte) error { } version, err = strconv.Atoi(s) if err != nil { - return fmt.Errorf("error parsing manifest_version: %w", err) + return fmt.Errorf("error parsing manifest_version '%s': %w", s, err) } if version > ManifestLatestVersion { From 109524166c661ab604f5be69cc78f440ae8b5853 Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 16:56:32 +0100 Subject: [PATCH 09/14] fix(manifest): notify user of invalid syntax --- pkg/manifest/file.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index 8cd5426bb..413b1053a 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -65,13 +65,13 @@ func (f *File) Read(path string) (err error) { // Disabling as we need to load the fastly.toml from the user's file system. // This file is decoded into a predefined struct, any unrecognised fields are dropped. /* #nosec */ - data, err := os.ReadFile(path) + tree, err := toml.LoadFile(path) if err != nil { f.logErr(err) return err } - err = toml.Unmarshal(data, f) + err = tree.Unmarshal(f) if err != nil { f.logErr(err) return err @@ -97,6 +97,11 @@ func (f *File) Read(path string) (err error) { return fsterr.ErrIncompatibleManifestVersion } + if dt := tree.Get("setup.dictionaries"); dt != nil { + text.Warning(f.output, "Your fastly.toml manifest contains `[setup.dictionaries]`, which should be updated to `[setup.config_stores]`. Refer to the documentation at https://developer.fastly.com/reference/compute/fastly-toml/") + text.Break(f.output) + } + f.exists = true return nil From 2e4914754fb81a92ad695b031a12992d7e69aa5b Mon Sep 17 00:00:00 2001 From: Integralist Date: Thu, 13 Apr 2023 17:27:16 +0100 Subject: [PATCH 10/14] test(manifest): validate WARNING is displayed --- pkg/manifest/manifest_test.go | 22 +++++++++++++++++-- .../testdata/fastly-warning-dictionaries.toml | 18 +++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 pkg/manifest/testdata/fastly-warning-dictionaries.toml diff --git a/pkg/manifest/manifest_test.go b/pkg/manifest/manifest_test.go index d94adec14..88d035e3d 100644 --- a/pkg/manifest/manifest_test.go +++ b/pkg/manifest/manifest_test.go @@ -11,6 +11,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/cli/pkg/threadsafe" "github.com/google/go-cmp/cmp" toml "github.com/pelletier/go-toml" ) @@ -21,6 +22,7 @@ func TestManifest(t *testing.T) { valid bool expectedError error wantRemediationError string + expectedOutput string }{ "valid: semver": { manifest: "fastly-valid-semver.toml", @@ -44,6 +46,11 @@ func TestManifest(t *testing.T) { valid: false, expectedError: fsterr.ErrUnrecognisedManifestVersion, }, + "warning: dictionaries now replaced with config_stores": { + manifest: "fastly-warning-dictionaries.toml", + valid: true, // we display a warning but we don't exit command execution + expectedOutput: "WARNING: Your fastly.toml manifest contains `[setup.dictionaries]`", + }, } // NOTE: some of the fixture files are overwritten by the application logic @@ -80,9 +87,12 @@ func TestManifest(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - var m manifest.File + var ( + m manifest.File + stdout threadsafe.Buffer + ) m.SetErrLog(fsterr.Log) - m.SetOutput(os.Stdout) + m.SetOutput(&stdout) path, err := filepath.Abs(filepath.Join(prefix, tc.manifest)) if err != nil { @@ -106,6 +116,14 @@ func TestManifest(t *testing.T) { if m.ManifestVersion != manifest.ManifestLatestVersion { t.Fatalf("manifest_version '%d' doesn't match latest '%d'", m.ManifestVersion, manifest.ManifestLatestVersion) } + + output := stdout.String() + + t.Log(output) + + if tc.expectedOutput != "" && !strings.Contains(output, tc.expectedOutput) { + t.Fatalf("got: %s, want: %s", output, tc.expectedOutput) + } }) } } diff --git a/pkg/manifest/testdata/fastly-warning-dictionaries.toml b/pkg/manifest/testdata/fastly-warning-dictionaries.toml new file mode 100644 index 000000000..7467bf9ec --- /dev/null +++ b/pkg/manifest/testdata/fastly-warning-dictionaries.toml @@ -0,0 +1,18 @@ +manifest_version = 2 +name = "Default Rust template" +description = "Default package template for Rust based edge compute projects." +authors = ["example "] +language = "rust" + +[setup.dictionaries] + +[setup.dictionaries.service_config] +description = "Configuration data for my service" + +[setup.dictionaries.service_config.items] + +[setup.dictionaries.service_config.items.s3-primary-host] +value = "eu-west-2" + +[setup.dictionaries.service_config.items.s3-fallback-host] +value = "us-west-1" From d69f4105f750a35814bfbaf5c4967a880efdd80a Mon Sep 17 00:00:00 2001 From: Integralist Date: Fri, 14 Apr 2023 15:40:22 +0100 Subject: [PATCH 11/14] docs(manifest): mention env var lookup --- pkg/manifest/data.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/manifest/data.go b/pkg/manifest/data.go index b7e2dfdcc..8607eeb43 100644 --- a/pkg/manifest/data.go +++ b/pkg/manifest/data.go @@ -12,7 +12,7 @@ import ( // // If the same parameter is defined in multiple places, it is resolved according // to the following priority order: the manifest file (lowest priority) and then -// explicit flags (highest priority). +// environment variables (where applicable), and explicit flags (highest priority). type Data struct { File File Flag Flag From e30521e09f8bc686e5c72b331b70a7aa97f04cc8 Mon Sep 17 00:00:00 2001 From: Integralist Date: Fri, 14 Apr 2023 15:51:45 +0100 Subject: [PATCH 12/14] refactor(manifest): simplify manifest_version parsing --- pkg/manifest/file.go | 4 ---- pkg/manifest/version.go | 17 ++--------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index 413b1053a..e19357a55 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -93,10 +93,6 @@ func (f *File) Read(path string) (err error) { } } - if f.ManifestVersion < ManifestLatestVersion { - return fsterr.ErrIncompatibleManifestVersion - } - if dt := tree.Get("setup.dictionaries"); dt != nil { text.Warning(f.output, "Your fastly.toml manifest contains `[setup.dictionaries]`, which should be updated to `[setup.config_stores]`. Refer to the documentation at https://developer.fastly.com/reference/compute/fastly-toml/") text.Break(f.output) diff --git a/pkg/manifest/version.go b/pkg/manifest/version.go index 57e8b6375..dd4b6f77c 100644 --- a/pkg/manifest/version.go +++ b/pkg/manifest/version.go @@ -37,21 +37,6 @@ type Version int func (v *Version) UnmarshalText(txt []byte) error { s := string(txt) - if i, err := strconv.Atoi(s); err == nil { - *v = Version(i) - return nil - } - - if f, err := strconv.ParseFloat(s, 32); err == nil { - intfl := int(f) - if intfl == 0 { - *v = ManifestLatestVersion - } else { - *v = Version(intfl) - } - return nil - } - // Presumes semver value (e.g. 1.0.0, 0.1.0 or 0.1) // Major is converted to integer if != zero. // Otherwise if Major == zero, then ignore Minor/Patch and set to latest version. @@ -66,6 +51,7 @@ func (v *Version) UnmarshalText(txt []byte) error { s = strconv.Itoa(ManifestLatestVersion) } } + version, err = strconv.Atoi(s) if err != nil { return fmt.Errorf("error parsing manifest_version '%s': %w", s, err) @@ -74,6 +60,7 @@ func (v *Version) UnmarshalText(txt []byte) error { if version > ManifestLatestVersion { return fsterr.ErrUnrecognisedManifestVersion } + *v = Version(version) return nil } From 8cbae99ee1dd77f6f659a61dfe146b088b750ff1 Mon Sep 17 00:00:00 2001 From: Integralist Date: Fri, 14 Apr 2023 15:59:56 +0100 Subject: [PATCH 13/14] fix(setup/config-store): add resource link logic --- pkg/commands/compute/deploy_test.go | 3 + pkg/commands/compute/setup/config_store.go | 93 ++++++++++++++-------- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/pkg/commands/compute/deploy_test.go b/pkg/commands/compute/deploy_test.go index 8a71c06b0..a0c30d3d7 100644 --- a/pkg/commands/compute/deploy_test.go +++ b/pkg/commands/compute/deploy_test.go @@ -1219,6 +1219,7 @@ func TestDeploy(t *testing.T) { CreateBackendFn: createBackendOK, CreateConfigStoreFn: createConfigStoreOK, CreateConfigStoreItemFn: createConfigStoreItemOK, + CreateResourceFn: createResourceOK, CreateDomainFn: createDomainOK, CreateServiceFn: createServiceOK, GetPackageFn: getPackageOk, @@ -1278,6 +1279,7 @@ func TestDeploy(t *testing.T) { CreateBackendFn: createBackendOK, CreateConfigStoreFn: createConfigStoreOK, CreateConfigStoreItemFn: createConfigStoreItemOK, + CreateResourceFn: createResourceOK, CreateDomainFn: createDomainOK, CreateServiceFn: createServiceOK, GetPackageFn: getPackageOk, @@ -1331,6 +1333,7 @@ func TestDeploy(t *testing.T) { CreateBackendFn: createBackendOK, CreateConfigStoreFn: createConfigStoreOK, CreateConfigStoreItemFn: createConfigStoreItemOK, + CreateResourceFn: createResourceOK, CreateDomainFn: createDomainOK, CreateServiceFn: createServiceOK, GetPackageFn: getPackageOk, diff --git a/pkg/commands/compute/setup/config_store.go b/pkg/commands/compute/setup/config_store.go index 9774ea785..090bd07b4 100644 --- a/pkg/commands/compute/setup/config_store.go +++ b/pkg/commands/compute/setup/config_store.go @@ -46,13 +46,13 @@ type ConfigStoreItem struct { } // Configure prompts the user for specific values related to the service resource. -func (d *ConfigStores) Configure() error { - for name, settings := range d.Setup { - if !d.AcceptDefaults && !d.NonInteractive { - text.Break(d.Stdout) - text.Output(d.Stdout, "Configuring config store '%s'", name) +func (o *ConfigStores) Configure() error { + for name, settings := range o.Setup { + if !o.AcceptDefaults && !o.NonInteractive { + text.Break(o.Stdout) + text.Output(o.Stdout, "Configuring config store '%s'", name) if settings.Description != "" { - text.Output(d.Stdout, settings.Description) + text.Output(o.Stdout, settings.Description) } } @@ -70,15 +70,15 @@ func (d *ConfigStores) Configure() error { err error ) - if !d.AcceptDefaults && !d.NonInteractive { - text.Break(d.Stdout) - text.Output(d.Stdout, "Create a config store key called '%s'", key) + if !o.AcceptDefaults && !o.NonInteractive { + text.Break(o.Stdout) + text.Output(o.Stdout, "Create a config store key called '%s'", key) if item.Description != "" { - text.Output(d.Stdout, item.Description) + text.Output(o.Stdout, item.Description) } - text.Break(d.Stdout) + text.Break(o.Stdout) - value, err = text.Input(d.Stdout, prompt, d.Stdin) + value, err = text.Input(o.Stdout, prompt, o.Stdin) if err != nil { return fmt.Errorf("error reading prompt input: %w", err) } @@ -94,7 +94,7 @@ func (d *ConfigStores) Configure() error { }) } - d.required = append(d.required, ConfigStore{ + o.required = append(o.required, ConfigStore{ Name: name, Items: items, }) @@ -104,70 +104,99 @@ func (d *ConfigStores) Configure() error { } // Create calls the relevant API to create the service resource(s). -func (d *ConfigStores) Create() error { - if d.Spinner == nil { +func (o *ConfigStores) Create() error { + if o.Spinner == nil { return errors.RemediationError{ Inner: fmt.Errorf("internal logic error: no spinner configured for setup.ConfigStores"), Remediation: errors.BugRemediation, } } - for _, store := range d.required { - err := d.Spinner.Start() + for _, store := range o.required { + err := o.Spinner.Start() if err != nil { return err } msg := fmt.Sprintf("Creating config store '%s'", store.Name) - d.Spinner.Message(msg + "...") + o.Spinner.Message(msg + "...") - cs, err := d.APIClient.CreateConfigStore(&fastly.CreateConfigStoreInput{ + cs, err := o.APIClient.CreateConfigStore(&fastly.CreateConfigStoreInput{ Name: store.Name, }) if err != nil { - d.Spinner.StopFailMessage(msg) - err := d.Spinner.StopFail() + o.Spinner.StopFailMessage(msg) + err := o.Spinner.StopFail() if err != nil { return err } return fmt.Errorf("error creating config store: %w", err) } - d.Spinner.StopMessage(msg) - err = d.Spinner.Stop() + o.Spinner.StopMessage(msg) + err = o.Spinner.Stop() if err != nil { return err } if len(store.Items) > 0 { for _, item := range store.Items { - err := d.Spinner.Start() + err := o.Spinner.Start() if err != nil { return err } msg := fmt.Sprintf("Creating config store item '%s'", item.Key) - d.Spinner.Message(msg + "...") + o.Spinner.Message(msg + "...") - _, err = d.APIClient.CreateConfigStoreItem(&fastly.CreateConfigStoreItemInput{ + _, err = o.APIClient.CreateConfigStoreItem(&fastly.CreateConfigStoreItemInput{ StoreID: cs.ID, Key: item.Key, Value: item.Value, }) if err != nil { - d.Spinner.StopFailMessage(msg) - err := d.Spinner.StopFail() + o.Spinner.StopFailMessage(msg) + err := o.Spinner.StopFail() if err != nil { return err } return fmt.Errorf("error creating config store item: %w", err) } - d.Spinner.StopMessage(msg) - err = d.Spinner.Stop() + o.Spinner.StopMessage(msg) + err = o.Spinner.Stop() if err != nil { return err } } } + + err = o.Spinner.Start() + if err != nil { + return err + } + msg = fmt.Sprintf("Creating resource link between service and config store '%s'...", cs.Name) + o.Spinner.Message(msg) + + // IMPORTANT: We need to link the config store to the C@E Service. + _, err = o.APIClient.CreateResource(&fastly.CreateResourceInput{ + ServiceID: o.ServiceID, + ServiceVersion: o.ServiceVersion, + Name: fastly.String(cs.Name), + ResourceID: fastly.String(cs.ID), + }) + if err != nil { + o.Spinner.StopFailMessage(msg) + err := o.Spinner.StopFail() + if err != nil { + return err + } + return fmt.Errorf("error creating resource link between the service '%s' and the config store '%s': %w", o.ServiceID, store.Name, err) + } + + o.Spinner.StopMessage(msg) + err = o.Spinner.Stop() + if err != nil { + return err + } } return nil @@ -175,6 +204,6 @@ func (d *ConfigStores) Create() error { // Predefined indicates if the service resource has been specified within the // fastly.toml file using a [setup] configuration block. -func (d *ConfigStores) Predefined() bool { - return len(d.Setup) > 0 +func (o *ConfigStores) Predefined() bool { + return len(o.Setup) > 0 } From 58e6012a35c8dcc6d92c3b1378c14e89296c578c Mon Sep 17 00:00:00 2001 From: Integralist Date: Fri, 14 Apr 2023 16:18:58 +0100 Subject: [PATCH 14/14] breaking(manifest): bump manifest_version to latest version --- pkg/manifest/manifest.go | 2 +- pkg/manifest/testdata/fastly-missing-spec-url.toml | 2 +- pkg/manifest/testdata/fastly-valid-integer.toml | 2 +- pkg/manifest/testdata/fastly-viceroy-update.toml | 2 +- pkg/manifest/testdata/fastly-warning-dictionaries.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index 552638ca2..aaea26d76 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -13,7 +13,7 @@ const ( // // NOTE: The CLI is the primary consumer of the fastly.toml manifest so its // code is typically coupled to the specification. - ManifestLatestVersion = 2 + ManifestLatestVersion = 3 // FilePermissions represents a read/write file mode. FilePermissions = 0o666 diff --git a/pkg/manifest/testdata/fastly-missing-spec-url.toml b/pkg/manifest/testdata/fastly-missing-spec-url.toml index 99b315130..75770221e 100644 --- a/pkg/manifest/testdata/fastly-missing-spec-url.toml +++ b/pkg/manifest/testdata/fastly-missing-spec-url.toml @@ -1,4 +1,4 @@ -manifest_version = 2 +manifest_version = 3 name = "Default Rust template" description = "Default package template for Rust based edge compute projects." authors = ["phamann "] diff --git a/pkg/manifest/testdata/fastly-valid-integer.toml b/pkg/manifest/testdata/fastly-valid-integer.toml index 99b315130..75770221e 100644 --- a/pkg/manifest/testdata/fastly-valid-integer.toml +++ b/pkg/manifest/testdata/fastly-valid-integer.toml @@ -1,4 +1,4 @@ -manifest_version = 2 +manifest_version = 3 name = "Default Rust template" description = "Default package template for Rust based edge compute projects." authors = ["phamann "] diff --git a/pkg/manifest/testdata/fastly-viceroy-update.toml b/pkg/manifest/testdata/fastly-viceroy-update.toml index 60bf2d54e..8f2b0fad4 100644 --- a/pkg/manifest/testdata/fastly-viceroy-update.toml +++ b/pkg/manifest/testdata/fastly-viceroy-update.toml @@ -4,7 +4,7 @@ authors = ["phamann "] description = "Default package template for Rust based edge compute projects." language = "rust" -manifest_version = 2 +manifest_version = 3 name = "Default Rust template" [local_server] diff --git a/pkg/manifest/testdata/fastly-warning-dictionaries.toml b/pkg/manifest/testdata/fastly-warning-dictionaries.toml index 7467bf9ec..1a5c2bff2 100644 --- a/pkg/manifest/testdata/fastly-warning-dictionaries.toml +++ b/pkg/manifest/testdata/fastly-warning-dictionaries.toml @@ -1,4 +1,4 @@ -manifest_version = 2 +manifest_version = 3 name = "Default Rust template" description = "Default package template for Rust based edge compute projects." authors = ["example "]