Skip to content

feat: allow ValuesFile from GCS #9182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions pkg/skaffold/gcs/gsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ import (

const GsutilExec = "gsutil"

type Gsutil struct{}
type Gsutil interface {
Copy(ctx context.Context, src, dst string, recursive bool) error
}

type gsutil struct{}

// NewGsutil returns a gsutil client.
func NewGsutil() Gsutil {
return &gsutil{}
}

// Copy calls `gsutil cp [-r] <source_url> <destination_url>
func (g *Gsutil) Copy(ctx context.Context, src, dst string, recursive bool) error {
func (g *gsutil) Copy(ctx context.Context, src, dst string, recursive bool) error {
args := []string{"cp"}
if recursive {
args = append(args, "-r")
Expand Down
2 changes: 1 addition & 1 deletion pkg/skaffold/gcs/gsutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestCopy(t *testing.T) {
testutil.Run(t, test.description, func(t *testutil.T) {
t.Override(&util.DefaultExecCommand, test.commands)

gcs := Gsutil{}
gcs := NewGsutil()
err := gcs.Copy(context.Background(), test.src, test.dst, test.recursive)

t.CheckError(test.shouldErr, err)
Expand Down
41 changes: 40 additions & 1 deletion pkg/skaffold/helm/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package helm
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
Expand All @@ -27,13 +30,19 @@ import (

"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/constants"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/gcs"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util"
maps "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util/map"
)

const (
gcsPrefix = "gs://"
valueFileFromGCS = "valuefiles_from_gcs"
)

// ConstructOverrideArgs creates the command line arguments for overrides
func ConstructOverrideArgs(r *latest.HelmRelease, builds []graph.Artifact, args []string, manifestOverrides map[string]string) ([]string, error) {
for _, k := range maps.SortKeys(r.SetValues) {
Expand Down Expand Up @@ -85,8 +94,26 @@ func ConstructOverrideArgs(r *latest.HelmRelease, builds []graph.Artifact, args
args = append(args, "--set", fmt.Sprintf("%s=%s", expandedKey, v))
}

gcs := gcs.NewGsutil()

for _, v := range r.ValuesFiles {
exp, err := homedir.Expand(v)
tempValueFile := v

// if the file starts with gs:// then download it in tmp dir
if strings.HasPrefix(v, gcsPrefix) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like it'll be better to contribute to the helm repository

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skaffold has already been integrated with GCP, so I thought to make the change here.

tempDir, err := os.MkdirTemp("", valueFileFromGCS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the condition's body can be extracted for better readability

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, yes separated to the new function.

if err != nil {
return nil, fmt.Errorf("failed to create the tmp directory: %w", err)
}

if extractedFilePath, err := extractValueFileFromGCS(v, tempDir, gcs); err != nil {
return nil, err
} else {
tempValueFile = extractedFilePath
}
}

exp, err := homedir.Expand(tempValueFile)
if err != nil {
return nil, fmt.Errorf("unable to expand %q: %w", v, err)
}
Expand Down Expand Up @@ -151,3 +178,15 @@ func envVarForImage(imageName string, digest string) map[string]string {
customMap["IMAGE_FULLY_QUALIFIED"] = digest
return customMap
}

// Copy the value file from the GCS bucket if it starts with gs://
func extractValueFileFromGCS(v, tempDir string, gcs gcs.Gsutil) (string, error) {
// get a filename from gcs
tempValueFile := filepath.Join(tempDir, path.Base(v))

if err := gcs.Copy(context.TODO(), v, tempValueFile, false); err != nil {
return "", fmt.Errorf("failed to copy valuesFile from GCS: %w", err)
}

return tempValueFile, nil
}
58 changes: 58 additions & 0 deletions pkg/skaffold/helm/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ limitations under the License.
package helm

import (
"context"
"fmt"
"path/filepath"
"strings"
"testing"

"github.com/GoogleContainerTools/skaffold/v2/testutil"
Expand Down Expand Up @@ -122,3 +126,57 @@ func TestSanitizeFilePath(t *testing.T) {
})
}
}

// MockGsutil implements gcs.Gsutil with a controlled Copy method
type mockGsutil struct{}

// Copy extracts the filename and returns success or failure based on the filename.
func (m *mockGsutil) Copy(ctx context.Context, src string, dst string, recursive bool) error {
// Extract the filename from the source path
filename := filepath.Base(src)

// If the filename contains "not_exist", return an error (simulate failure)
if strings.Contains(filename, "not_exist") {
return fmt.Errorf("file not found: %s", src)
}

// Otherwise, return success
return nil
}

func TestExtractValueFileFromGCS(t *testing.T) {
tempDir := t.TempDir()

tests := []struct {
description string
src string
dst string
shouldErr bool
}{
{
description: "copy file from GCS",
src: "gs://bucket/path/to/values.yaml",
dst: filepath.Join(tempDir, "values.yaml"),
shouldErr: false,
},
{
description: "fail to copy file from GCS",
src: "gs://bucket/path/to/not_exist_values.yaml",
dst: filepath.Join(tempDir, "not_exist_values.yaml"),
shouldErr: true,
},
}

for _, test := range tests {
testutil.Run(t, test.description, func(t *testutil.T) {
result, err := extractValueFileFromGCS(test.src, tempDir, &mockGsutil{})
// Check if the error status matches the expected result
t.CheckError(test.shouldErr, err)

// If no error, validate the copied file path
if err == nil {
t.CheckDeepEqual(result, test.dst)
}
})
}
}
Loading