mirror of https://github.com/pulumi/pulumi.git
170 lines
5.3 KiB
Go
170 lines
5.3 KiB
Go
// 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
|
|
}
|