// Copyright 2016-2023, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package diy import ( "context" "fmt" "path/filepath" "github.com/pulumi/pulumi/sdk/v3/go/common/env" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" "gocloud.dev/gcerrors" "gopkg.in/yaml.v3" ) // Path inside the bucket where we store the metadata file. var pulumiMetaPath = filepath.Join(workspace.BookkeepingDir, "meta.yaml") // pulumiMeta holds the contents of the .pulumi/meta.yaml file // in a diy backend. // // This file specifies metadata for the backend, // including a version number that the backend can use // to maintain compatibility with older versions of the CLI. // // The metadata file is not written for legacy layouts. // However, there was a short period of time where it was written, // so we should still allow for Version 0 when reading these files. type pulumiMeta struct { // Version is the current version of the state store. // // Version 0 is the starting version. // It does not support project-scoped stacks. // Version 1 adds support for project-scoped stacks. // // Does not use "omitempty" to differentiate // between a missing field and a zero value. Version int `json:"version" yaml:"version"` } // ensurePulumiMeta loads the Pulumi state metadata file from the bucket. // // Unlike [readPulumiMeta], // the result of this function will always be non-nil if the error is nil. // // If the bucket is empty, this will create a new metadata file // with the latest version number. // This can be overridden by setting the environment variable // "PULUMI_diy_BACKEND_LEGACY_LAYOUT" to "1". // ensurePulumiMeta uses the provided 'getenv' function // to read the environment variable. func ensurePulumiMeta(ctx context.Context, b Bucket, e env.Env) (*pulumiMeta, error) { meta, err := readPulumiMeta(ctx, b) if err != nil { return nil, err } if meta != nil { return meta, nil } // If there's no metadata file, we need to create one. // The version we pick for the new file decides how we lay out the state. // // - Version 0 is legacy mode, which is the old layout. // To avoid breaking old stacks, we want to use version 0 // if the bucket is not empty. // // - Version 1 added support for project-scoped stacks. // For entirely new buckets, we'll use version 1 // to give new users access to the latest features. refs, err := newLegacyReferenceStore(b).ListReferences(ctx) if err != nil { // If there's an error listing don't fail, just don't print the warnings return nil, err } useLegacy := len(refs) > 0 if !useLegacy { // Allow opting into legacy mode for new states // by setting the environment variable. useLegacy = e.GetBool(env.DIYBackendLegacyLayout) } if useLegacy { meta = &pulumiMeta{Version: 0} } else { meta = &pulumiMeta{Version: 1} } // Implementation detail: // For version 0, WriteTo won't write the metadata file. // See [pulumiMeta.WriteTo] for details on why. if err := meta.WriteTo(ctx, b); err != nil { return nil, err } return meta, nil } // readPulumiMeta loads the Pulumi state metadata from the bucket. // If the file does not exist, it returns nil and no error. func readPulumiMeta(ctx context.Context, b Bucket) (*pulumiMeta, error) { metaBody, err := b.ReadAll(ctx, pulumiMetaPath) if err != nil { if gcerrors.Code(err) == gcerrors.NotFound { return nil, nil } return nil, fmt.Errorf("read %q: %w", pulumiMetaPath, err) } // State is a copy of the pulumiMeta shape, // but with pointers to fields where we need to differentiate // between a missing field and a zero value. // Don't use pointers for fields where the zero value is invalid. // // This is necessary because the YAML unmarshaler // will read a zero value for a missing field or an empty file. var state struct { // Version 0 is valid, so we need to use a pointer. Version *int `yaml:"version"` } if err := yaml.Unmarshal(metaBody, &state); err != nil { return nil, fmt.Errorf("corrupt store: unmarshal %q: %w", pulumiMetaPath, err) } if state.Version == nil { return nil, fmt.Errorf("corrupt store: missing version in %q", pulumiMetaPath) } return &pulumiMeta{ Version: *state.Version, }, nil } // WriteTo writes the metadata to the bucket, overwriting any existing metadata. func (m *pulumiMeta) WriteTo(ctx context.Context, b Bucket) error { if m.Version == 0 { // We don't want to write a metadata file // for legacy layouts. // // This allows for cases where a user has // strict permission controls on their bucket, // and doesn't expect a file outside .pulumi/stacks/. return nil } bs, err := yaml.Marshal(m) contract.AssertNoErrorf(err, "Could not marshal diy.pulumiMeta to YAML") if err := b.WriteAll(ctx, pulumiMetaPath, bs, nil); err != nil { return fmt.Errorf("write %q: %w", pulumiMetaPath, err) } return nil }