func newEnviron(cloud environs.CloudSpec, cfg *config.Config) (*environ, error) { ecfg, err := newValidConfig(cfg, configDefaults) if err != nil { return nil, errors.Annotate(err, "invalid config") } client, err := newClient(cloud) if err != nil { return nil, errors.Annotatef(err, "failed to create new client") } namespace, err := instance.NewNamespace(cfg.UUID()) if err != nil { return nil, errors.Trace(err) } env := &environ{ name: ecfg.Name(), cloud: cloud, ecfg: ecfg, client: client, namespace: namespace, } return env, nil }
// prepare is the internal version of Prepare - it prepares the // environment but does not open it. func (p *environProvider) prepare(cfg *config.Config) (*config.Config, error) { ecfg, err := p.newConfig(cfg) if err != nil { return nil, err } p.mu.Lock() defer p.mu.Unlock() name := cfg.Name() if ecfg.stateId() != noStateId { return cfg, nil } if ecfg.stateServer() && len(p.state) != 0 { for _, old := range p.state { panic(fmt.Errorf("cannot share a state between two dummy environs; old %q; new %q", old.name, name)) } } // The environment has not been prepared, // so create it and set its state identifier accordingly. state := newState(name, p.ops, p.statePolicy) p.maxStateId++ state.id = p.maxStateId p.state[state.id] = state attrs := map[string]interface{}{"state-id": fmt.Sprint(state.id)} if ecfg.stateServer() { attrs["api-port"] = state.listenAPI() } return cfg.Apply(attrs) }
func newEnviron(spec environs.CloudSpec, cfg *config.Config, newRawProvider newRawProviderFunc) (*environ, error) { ecfg, err := newValidConfig(cfg) if err != nil { return nil, errors.Annotate(err, "invalid config") } namespace, err := instance.NewNamespace(cfg.UUID()) if err != nil { return nil, errors.Trace(err) } raw, err := newRawProvider(spec) if err != nil { return nil, errors.Trace(err) } env := &environ{ name: ecfg.Name(), uuid: ecfg.UUID(), raw: raw, namespace: namespace, ecfg: ecfg, } env.base = common.DefaultProvider{Env: env} //TODO(wwitzel3) make sure we are also cleaning up profiles during destroy if err := env.initProfile(); err != nil { return nil, errors.Trace(err) } return env, nil }
// DefaultVersions returns a slice of unique 'versions' for the current // environment's preferred series and host architecture, as well supported LTS // series for the host architecture. Additionally, it ensures that 'versions' // for amd64 are returned if that is not the current host's architecture. func DefaultVersions(conf *config.Config) []version.Binary { var versions []version.Binary supported := series.SupportedLts() defaultSeries := set.NewStrings(supported...) defaultSeries.Add(config.PreferredSeries(conf)) defaultSeries.Add(series.HostSeries()) agentVersion, set := conf.AgentVersion() if !set { agentVersion = jujuversion.Current } for _, s := range defaultSeries.Values() { versions = append(versions, version.Binary{ Number: agentVersion, Arch: arch.HostArch(), Series: s, }) if arch.HostArch() != "amd64" { versions = append(versions, version.Binary{ Number: agentVersion, Arch: "amd64", Series: s, }) } } return versions }
// defaultStoragePool returns the default storage pool for the environment. // The default pool is either user specified, or one that is registered by the provider itself. func defaultStoragePool(cfg *config.Config, kind storage.StorageKind, cons StorageConstraints) (string, error) { switch kind { case storage.StorageKindBlock: loopPool := string(provider.LoopProviderType) emptyConstraints := StorageConstraints{} if cons == emptyConstraints { // No constraints at all: use loop. return loopPool, nil } // Either size or count specified, use env default. defaultPool, ok := cfg.StorageDefaultBlockSource() if !ok { defaultPool = loopPool } return defaultPool, nil case storage.StorageKindFilesystem: rootfsPool := string(provider.RootfsProviderType) emptyConstraints := StorageConstraints{} if cons == emptyConstraints { return rootfsPool, nil } // TODO(axw) add env configuration for default // filesystem source, prefer that. defaultPool, ok := cfg.StorageDefaultBlockSource() if !ok { defaultPool = rootfsPool } return defaultPool, nil } return "", ErrNoDefaultStoragePool }
func validateConfig(cfg, old *config.Config) (*environConfig, error) { // Check for valid changes for the base config values. if err := config.Validate(cfg, old); err != nil { return nil, err } validated, err := cfg.ValidateUnknownAttrs(configFields, configDefaults) if err != nil { return nil, err } ecfg := &environConfig{cfg, validated} if vpcID := ecfg.vpcID(); isVPCIDSetButInvalid(vpcID) { return nil, fmt.Errorf("vpc-id: %q is not a valid AWS VPC ID", vpcID) } else if !isVPCIDSet(vpcID) && ecfg.forceVPCID() { return nil, fmt.Errorf("cannot use vpc-id-force without specifying vpc-id as well") } if old != nil { attrs := old.UnknownAttrs() if vpcID, _ := attrs["vpc-id"].(string); vpcID != ecfg.vpcID() { return nil, fmt.Errorf("cannot change vpc-id from %q to %q", vpcID, ecfg.vpcID()) } if forceVPCID, _ := attrs["vpc-id-force"].(bool); forceVPCID != ecfg.forceVPCID() { return nil, fmt.Errorf("cannot change vpc-id-force from %v to %v", forceVPCID, ecfg.forceVPCID()) } } // ssl-hostname-verification cannot be disabled if !ecfg.SSLHostnameVerification() { return nil, fmt.Errorf("disabling ssh-hostname-verification is not supported") } return ecfg, nil }
// SecretAttrs filters the supplied configuration returning only values // which are considered sensitive. All of the values of these secret // attributes need to be strings. func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { logger.Infof("filtering secret attributes for environment %q", cfg.Name()) // If you keep configSecretFields up to date, this method should Just Work. ecfg, err := validateConfig(cfg, nil) if err != nil { return nil, err } secretAttrs := map[string]string{} for _, field := range configSecretFields { if value, ok := ecfg.attrs[field]; ok { if stringValue, ok := value.(string); ok { secretAttrs[field] = stringValue } else { // All your secret attributes must be strings at the moment. Sorry. // It's an expedient and hopefully temporary measure that helps us // plug a security hole in the API. return nil, errors.Errorf( "secret %q field must have a string value; got %v", field, value, ) } } } return secretAttrs, nil }
// PrepareForBootstrap is specified in the EnvironProvider interface. func (prov *azureEnvironProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { // Ensure that internal configuration is not specified, and then set // what we can now. We only need to do this during bootstrap. Validate // will check for changes later. unknownAttrs := cfg.UnknownAttrs() for _, key := range internalConfigAttributes { if _, ok := unknownAttrs[key]; ok { return nil, errors.Errorf(`internal config %q must not be specified`, key) } } // Record the UUID that will be used for the controller environment. cfg, err := cfg.Apply(map[string]interface{}{ configAttrControllerResourceGroup: resourceGroupName(cfg), }) if err != nil { return nil, errors.Annotate(err, "recording controller-resource-group") } env, err := prov.Open(cfg) if err != nil { return nil, errors.Trace(err) } if ctx.ShouldVerifyCredentials() { if err := verifyCredentials(env.(*azureEnviron)); err != nil { return nil, errors.Trace(err) } } return env, nil }
// finalizeConfig creates the config object from attributes, calls // PrepareForCreateEnvironment, and then finally validates the config // before returning it. func finalizeConfig(isAdmin bool, controllerCfg *config.Config, attrs map[string]interface{}) (*config.Config, error) { provider, err := environs.Provider(controllerCfg.Type()) if err != nil { return nil, errors.Trace(err) } // Controller admins creating models do not have to re-supply new secrets. // These may be copied from the controller model if not supplied. if isAdmin { maybeCopyControllerSecrets(provider, controllerCfg.AllAttrs(), attrs) } cfg, err := config.New(config.UseDefaults, attrs) if err != nil { return nil, errors.Annotate(err, "creating config from values failed") } cfg, err = provider.PrepareForCreateEnvironment(cfg) if err != nil { return nil, errors.Trace(err) } cfg, err = provider.Validate(cfg, nil) if err != nil { return nil, errors.Annotate(err, "provider validation failed") } return cfg, nil }
// Prepare prepares a new environment based on the provided configuration. // If the environment is already prepared, it behaves like New. func Prepare(cfg *config.Config, ctx BootstrapContext, store configstore.Storage) (Environ, error) { if p, err := Provider(cfg.Type()); err != nil { return nil, err } else if info, err := store.ReadInfo(cfg.Name()); errors.IsNotFound(errors.Cause(err)) { info = store.CreateInfo(cfg.Name()) if env, err := prepare(ctx, cfg, info, p); err == nil { return env, decorateAndWriteInfo(info, env.Config()) } else { if err := info.Destroy(); err != nil { logger.Warningf("cannot destroy newly created environment info: %v", err) } return nil, err } } else if err != nil { return nil, errors.Annotatef(err, "error reading environment info %q", cfg.Name()) } else if !info.Initialized() { return nil, errors.Errorf( "found uninitialized environment info for %q; environment preparation probably in progress or interrupted", cfg.Name(), ) } else if len(info.BootstrapConfig()) == 0 { return nil, errors.New("found environment info but no bootstrap config") } else { cfg, err = config.New(config.NoDefaults, info.BootstrapConfig()) if err != nil { return nil, errors.Annotate(err, "cannot parse bootstrap config") } return New(cfg) } }
func validateConfig(cfg, old *config.Config) (*environConfig, error) { // Check for valid changes for the base config values. if err := config.Validate(cfg, old); err != nil { return nil, err } newAttrs, err := cfg.ValidateUnknownAttrs(configFields, configDefaults) if err != nil { return nil, err } envConfig := &environConfig{cfg, newAttrs} // If an old config was supplied, check any immutable fields have not changed. if old != nil { oldEnvConfig, err := validateConfig(old, nil) if err != nil { return nil, err } for _, field := range configImmutableFields { if oldEnvConfig.attrs[field] != envConfig.attrs[field] { return nil, fmt.Errorf( "%s: cannot change from %v to %v", field, oldEnvConfig.attrs[field], envConfig.attrs[field], ) } } } // Check for missing fields. for _, field := range requiredFields { if nilOrEmptyString(envConfig.attrs[field]) { return nil, fmt.Errorf("%s: must not be empty", field) } } return envConfig, nil }
func (p manualProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { envConfig, err := p.validate(cfg, old) if err != nil { return nil, err } return cfg.Apply(envConfig.attrs) }
// newConfig builds a new environConfig from the provided Config // filling in default values, if any. It returns an error if the // resulting configuration is not valid. func newConfig(cfg, old *config.Config) (*environConfig, error) { // Ensure that the provided config is valid. if err := config.Validate(cfg, old); err != nil { return nil, errors.Trace(err) } attrs, err := cfg.ValidateUnknownAttrs(configFields, configDefaults) if err != nil { return nil, errors.Trace(err) } if old != nil { // There's an old configuration. Validate it so that any // default values are correctly coerced for when we check // the old values later. oldEcfg, err := newConfig(old, nil) if err != nil { return nil, errors.Annotatef(err, "invalid base config") } for _, attr := range configImmutableFields { oldv, newv := oldEcfg.attrs[attr], attrs[attr] if oldv != newv { return nil, errors.Errorf( "%s: cannot change from %v to %v", attr, oldv, newv, ) } } } ecfg := &environConfig{ config: cfg, attrs: attrs, } return ecfg, nil }
// InstanceTags returns the minimum set of tags that should be set on a // machine instance, if the provider supports them. func InstanceTags(cfg *config.Config, jobs []multiwatcher.MachineJob) map[string]string { instanceTags := tags.ResourceTags(names.NewModelTag(cfg.UUID()), cfg) if multiwatcher.AnyJobNeedsState(jobs...) { instanceTags[tags.JujuController] = "true" } return instanceTags }
// update applies changes from the provided config to the env config. // Changes to any immutable attributes result in an error. func (c *environConfig) update(cfg *config.Config) error { // Validate the updates. newValidConfig does not modify the "known" // config attributes so it is safe to call Validate here first. if err := config.Validate(cfg, c.Config); err != nil { return errors.Trace(err) } updates, err := newValidConfig(cfg, configDefaults) if err != nil { return errors.Trace(err) } // Check that no immutable fields have changed. attrs := updates.UnknownAttrs() for _, field := range configImmutableFields { if attrs[field] != c.attrs[field] { return errors.Errorf("%s: cannot change from %v to %v", field, c.attrs[field], attrs[field]) } } // Apply the updates. c.Config = cfg c.attrs = cfg.UnknownAttrs() return nil }
// newValidConfig builds a new environConfig from the provided Config // and returns it. The resulting config values are validated. func newValidConfig(cfg *config.Config, defaults map[string]interface{}) (*environConfig, error) { // Ensure that the provided config is valid. if err := config.Validate(cfg, nil); err != nil { return nil, errors.Trace(err) } // Apply the defaults and coerce/validate the custom config attrs. validated, err := cfg.ValidateUnknownAttrs(configFields, defaults) if err != nil { return nil, errors.Trace(err) } validCfg, err := cfg.Apply(validated) if err != nil { return nil, errors.Trace(err) } // Build the config. ecfg := newConfig(validCfg) // Do final validation. if err := ecfg.validate(); err != nil { return nil, errors.Trace(err) } return ecfg, nil }
func (joyentProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { newEcfg, err := validateConfig(cfg, old) if err != nil { return nil, fmt.Errorf("invalid Joyent provider config: %v", err) } return cfg.Apply(newEcfg.attrs) }
func (api *API) parseMetadataListFromParams(p params.CloudImageMetadataList, cfg *config.Config) []cloudimagemetadata.Metadata { results := make([]cloudimagemetadata.Metadata, len(p.Metadata)) for i, metadata := range p.Metadata { results[i] = cloudimagemetadata.Metadata{ MetadataAttributes: cloudimagemetadata.MetadataAttributes{ Stream: metadata.Stream, Region: metadata.Region, Version: metadata.Version, Series: metadata.Series, Arch: metadata.Arch, VirtType: metadata.VirtType, RootStorageType: metadata.RootStorageType, RootStorageSize: metadata.RootStorageSize, Source: metadata.Source, }, Priority: metadata.Priority, ImageId: metadata.ImageId, } // TODO (anastasiamac 2016-08-24) This is a band-aid solution. // Once correct value is read from simplestreams, this needs to go. // Bug# 1616295 if results[i].Stream == "" { results[i].Stream = cfg.ImageStream() } } return results }
func (s *ModelConfigSourceSuite) assertModelConfigValues(c *gc.C, modelCfg *config.Config, modelAttributes, controllerAttributes set.Strings) { expectedValues := make(config.ConfigValues) defaultAttributes := set.NewStrings() for defaultAttr := range config.ConfigDefaults() { defaultAttributes.Add(defaultAttr) } for attr, val := range modelCfg.AllAttrs() { source := "model" if defaultAttributes.Contains(attr) { source = "default" } if modelAttributes.Contains(attr) { source = "model" } if controllerAttributes.Contains(attr) { source = "controller" } expectedValues[attr] = config.ConfigValue{ Value: val, Source: source, } } sources, err := s.State.ModelConfigValues() c.Assert(err, jc.ErrorIsNil) c.Assert(sources, jc.DeepEquals, expectedValues) }
// NewModelConfig returns a new model config given a base (controller) config // and a set of attributes that will be specific to the new model, overriding // any non-restricted attributes in the base configuration. The resulting // config will be suitable for creating a new model in state. // // If "attrs" does not include a UUID, a new, random one will be generated // and added to the config. // // The config will be validated with the provider before being returned. func (c ModelConfigCreator) NewModelConfig( isAdmin bool, base *config.Config, attrs map[string]interface{}, ) (*config.Config, error) { if err := c.checkVersion(base, attrs); err != nil { return nil, errors.Trace(err) } // Before comparing any values, we need to push the config through // the provider validation code. One of the reasons for this is that // numbers being serialized through JSON get turned into float64. The // schema code used in config will convert these back into integers. // However, before we can create a valid config, we need to make sure // we copy across fields from the main config that aren't there. baseAttrs := base.AllAttrs() restrictedFields, err := RestrictedProviderFields(base.Type()) if err != nil { return nil, errors.Trace(err) } for _, field := range restrictedFields { if _, ok := attrs[field]; !ok { if baseValue, ok := baseAttrs[field]; ok { attrs[field] = baseValue } } } // Generate a new UUID for the model as necessary, // and finalize the new config. if _, ok := attrs[config.UUIDKey]; !ok { uuid, err := utils.NewUUID() if err != nil { return nil, errors.Trace(err) } attrs[config.UUIDKey] = uuid.String() } cfg, err := finalizeConfig(isAdmin, base, attrs) if err != nil { return nil, errors.Trace(err) } attrs = cfg.AllAttrs() // Any values that would normally be copied from the controller // config can also be defined, but if they differ from the controller // values, an error is returned. for _, field := range restrictedFields { if value, ok := attrs[field]; ok { if serverValue := baseAttrs[field]; value != serverValue { return nil, errors.Errorf( "specified %s \"%v\" does not match controller \"%v\"", field, value, serverValue) } } } return cfg, nil }
func (t configTest) check(c *gc.C) { credential := cloud.NewCredential( cloud.AccessKeyAuthType, map[string]string{ "access-key": "x", "secret-key": "y", }, ) cloudSpec := environs.CloudSpec{ Type: "ec2", Name: "ec2test", Region: "us-east-1", Credential: &credential, } attrs := testing.FakeConfig().Merge(testing.Attrs{ "type": "ec2", }).Merge(t.config) cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) e, err := environs.New(environs.OpenParams{ Cloud: cloudSpec, Config: cfg, }) if t.change != nil { c.Assert(err, jc.ErrorIsNil) // Testing a change in configuration. var old, changed, valid *config.Config ec2env := e.(*environ) old = ec2env.ecfg().Config changed, err = old.Apply(t.change) c.Assert(err, jc.ErrorIsNil) // Keep err for validation below. valid, err = providerInstance.Validate(changed, old) if err == nil { err = ec2env.SetConfig(valid) } } if t.err != "" { c.Check(err, gc.ErrorMatches, t.err) return } c.Assert(err, jc.ErrorIsNil) ecfg := e.(*environ).ecfg() c.Assert(ecfg.Name(), gc.Equals, "testenv") c.Assert(ecfg.vpcID(), gc.Equals, t.vpcID) c.Assert(ecfg.forceVPCID(), gc.Equals, t.forceVPCID) if t.firewallMode != "" { c.Assert(ecfg.FirewallMode(), gc.Equals, t.firewallMode) } for name, expect := range t.expect { actual, found := ecfg.UnknownAttrs()[name] c.Check(found, jc.IsTrue) c.Check(actual, gc.Equals, expect) } }
// Open is specified in the EnvironProvider interface. func (prov *azureEnvironProvider) Open(cfg *config.Config) (environs.Environ, error) { logger.Debugf("opening model %q", cfg.Name()) environ, err := newEnviron(prov, cfg) if err != nil { return nil, errors.Annotate(err, "opening model") } return environ, nil }
// ensureAdminSecret returns a config with a non-empty admin-secret. func ensureAdminSecret(cfg *config.Config) (*config.Config, error) { if cfg.AdminSecret() != "" { return cfg, nil } return cfg.Apply(map[string]interface{}{ "admin-secret": randomKey(), }) }
// VolumeSource is defined on the Provider interface. func (e *ebsProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) { ec2, _, _, err := awsClients(environConfig) if err != nil { return nil, errors.Annotate(err, "creating AWS clients") } source := &ebsVolumeSource{ec2: ec2, envName: environConfig.Name()} return source, nil }
func base64yaml(m *config.Config) string { data, err := goyaml.Marshal(m.AllAttrs()) if err != nil { // can't happen, these values have been validated a number of times panic(err) } return base64.StdEncoding.EncodeToString(data) }
func (maasEnvironProvider) Open(cfg *config.Config) (environs.Environ, error) { logger.Debugf("opening environment %q.", cfg.Name()) env, err := NewEnviron(cfg) if err != nil { return nil, err } return env, nil }
// InstanceTags returns the minimum set of tags that should be set on a // machine instance, if the provider supports them. func InstanceTags(cfg *config.Config, jobs []multiwatcher.MachineJob) map[string]string { uuid, _ := cfg.UUID() instanceTags := tags.ResourceTags(names.NewEnvironTag(uuid), cfg) if multiwatcher.AnyJobNeedsState(jobs...) { instanceTags[tags.JujuStateServer] = "true" } return instanceTags }
func (t configTest) check(c *gc.C) { attrs := testing.FakeConfig().Merge(testing.Attrs{ "type": "ec2", }).Merge(t.config) cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) e, err := environs.New(cfg) if t.change != nil { c.Assert(err, jc.ErrorIsNil) // Testing a change in configuration. var old, changed, valid *config.Config ec2env := e.(*environ) old = ec2env.ecfg().Config changed, err = old.Apply(t.change) c.Assert(err, jc.ErrorIsNil) // Keep err for validation below. valid, err = providerInstance.Validate(changed, old) if err == nil { err = ec2env.SetConfig(valid) } } if t.err != "" { c.Check(err, gc.ErrorMatches, t.err) return } c.Assert(err, jc.ErrorIsNil) ecfg := e.(*environ).ecfg() c.Assert(ecfg.Name(), gc.Equals, "testenv") if t.region != "" { c.Assert(ecfg.region(), gc.Equals, t.region) } if t.accessKey != "" { c.Assert(ecfg.accessKey(), gc.Equals, t.accessKey) c.Assert(ecfg.secretKey(), gc.Equals, t.secretKey) expected := map[string]string{ "access-key": t.accessKey, "secret-key": t.secretKey, } c.Assert(err, jc.ErrorIsNil) actual, err := e.Provider().SecretAttrs(ecfg.Config) c.Assert(err, jc.ErrorIsNil) c.Assert(expected, gc.DeepEquals, actual) } else { c.Assert(ecfg.accessKey(), gc.DeepEquals, testAuth.AccessKey) c.Assert(ecfg.secretKey(), gc.DeepEquals, testAuth.SecretKey) } if t.firewallMode != "" { c.Assert(ecfg.FirewallMode(), gc.Equals, t.firewallMode) } for name, expect := range t.expect { actual, found := ecfg.UnknownAttrs()[name] c.Check(found, jc.IsTrue) c.Check(actual, gc.Equals, expect) } }
// SetConfig is specified in the Environ interface. func (env *azureEnviron) SetConfig(cfg *config.Config) error { env.mu.Lock() defer env.mu.Unlock() var old *config.Config if env.config != nil { old = env.config.Config } ecfg, err := validateConfig(cfg, old) if err != nil { return err } env.config = ecfg // Initialise clients. env.compute = compute.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) env.resources = resources.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) env.storage = storage.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) env.network = network.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) clients := map[string]*autorest.Client{ "azure.compute": &env.compute.Client, "azure.resources": &env.resources.Client, "azure.storage": &env.storage.Client, "azure.network": &env.network.Client, } if env.provider.config.Sender != nil { env.config.token.SetSender(env.provider.config.Sender) } for id, client := range clients { client.Authorizer = env.config.token logger := loggo.GetLogger(id) if env.provider.config.Sender != nil { client.Sender = env.provider.config.Sender } client.ResponseInspector = tracingRespondDecorator(logger) client.RequestInspector = tracingPrepareDecorator(logger) if env.provider.config.RequestInspector != nil { tracer := client.RequestInspector inspector := env.provider.config.RequestInspector client.RequestInspector = func(p autorest.Preparer) autorest.Preparer { p = tracer(p) p = inspector(p) return p } } } // Invalidate instance types when the location changes. if old != nil { oldLocation := old.UnknownAttrs()["location"].(string) if env.config.location != oldLocation { env.instanceTypes = nil } } return nil }
// checkEnvironConfig returns an error if the config is definitely invalid. func checkEnvironConfig(cfg *config.Config) error { if cfg.AdminSecret() != "" { return fmt.Errorf("admin-secret should never be written to the state") } if _, ok := cfg.AgentVersion(); !ok { return fmt.Errorf("agent-version must always be set in state") } return nil }