Repository: GoogleContainerTools/kpt
Pull request #3680 (2022-12-01 12:56:11)

dont template formula
- brew formula: replace text/template
- brew formula: use a fake http server in tests



Patch:
diff --git a/release/formula/main.go b/release/formula/main.go
index fdb4af1e..f534e133 100644
--- a/release/formula/main.go
+++ b/release/formula/main.go
@@ -15,103 +15,84 @@
 package main
 
 import (
-	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
 	"fmt"
+	"hash"
 	"io"
 	"net/http"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
-	"text/template"
 )
 
 func main() {
-	if len(os.Args) < 2 {
-		fmt.Fprintf(os.Stderr, "must specify new version\n")
-		os.Exit(1)
-	}
-	input := Input{Version: os.Args[1]}
-	var err error
-	input.Sha, err = getSha(input.Version)
+	err := run(context.Background())
 	if err != nil {
+		fmt.Fprintf(os.Stderr, "%v\n", err)
 		os.Exit(1)
 	}
+}
 
-	// generate the formula text
-	t, err := template.New("formula").Parse(formula)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v", err)
-		os.Exit(1)
+func run(ctx context.Context) error {
+	if len(os.Args) < 2 {
+		return fmt.Errorf("must specify new version")
 	}
 
-	// write the new formula
-	b := &bytes.Buffer{}
-	if err = t.Execute(b, input); err != nil {
-		fmt.Fprintf(os.Stderr, "%v", err)
-		os.Exit(1)
+	version := os.Args[1]
+	url := "https://github.com/GoogleContainerTools/kpt/archive/" + version + ".tar.gz"
+
+	formula, err := buildFormula(http.DefaultClient, url)
+	if err != nil {
+		return err
 	}
 
-	err = os.WriteFile(filepath.Join("Formula", "kpt.rb"), b.Bytes(), 0644)
+	err = os.WriteFile(filepath.Join("Formula", "kpt.rb"), []byte(formula), 0644)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v", err)
-		os.Exit(1)
+		return err
 	}
+	return nil
 }
 
-func getSha(version string) (string, error) {
-	// create the dir for the data
-	d, err := os.MkdirTemp("", "kpt-bin")
+func buildFormula(httpClient *http.Client, url string) (string, error) {
+	sha256, err := hashURL(httpClient, url, sha256.New())
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v", err)
 		return "", err
 	}
-	defer os.RemoveAll(d)
 
-	fmt.Println(
-		"fetching https://github.com/GoogleContainerTools/kpt/archive/" + version + ".tar.gz")
+	// generate the formula text
+	formula := formulaTemplate
+	formula = strings.ReplaceAll(formula, "{{url}}", url)
+	formula = strings.ReplaceAll(formula, "{{sha256}}", sha256)
+
+	return formula, nil
+}
+
+func hashURL(httpClient *http.Client, url string, hasher hash.Hash) (string, error) {
+	fmt.Printf("fetching %q\n", url)
+
 	// get the content
-	resp, err := http.Get(
-		"https://github.com/GoogleContainerTools/kpt/archive/" + version + ".tar.gz")
+	resp, err := httpClient.Get(url)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v", err)
-		return "", err
+		return "", fmt.Errorf("error getting %q: %w", url, err)
 	}
 	defer resp.Body.Close()
 
-	// write the file
-	func() {
-		out, err := os.Create(filepath.Join(d, version+".tar.gz"))
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "%v", err)
-			os.Exit(1)
-		}
-
-		if _, err = io.Copy(out, resp.Body); err != nil {
-			fmt.Fprintf(os.Stderr, "%v", err)
-			os.Exit(1)
-		}
-		out.Close()
-	}()
+	if resp.StatusCode != 200 {
+		return "", fmt.Errorf("unexpected response from %q: %v", url, resp.Status)
+	}
 
-	// calculate the sha
-	e := exec.Command("sha256sum", filepath.Join(d, version+".tar.gz"))
-	o, err := e.Output()
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v", err)
-		return "", err
+	if _, err := io.Copy(hasher, resp.Body); err != nil {
+		return "", fmt.Errorf("error hashing response from %q: %w", url, err)
 	}
-	parts := strings.Split(string(o), " ")
-	fmt.Println("new sha: " + parts[0])
-	return parts[0], nil
-}
 
-type Input struct {
-	Version string
-	Sha     string
+	// calculate the sha
+	hash := hasher.Sum(nil)
+	return hex.EncodeToString(hash), nil
 }
 
-const formula = `# Copyright 2019 Google LLC
+const formulaTemplate = `# Copyright 2019 Google LLC
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -128,8 +109,8 @@ const formula = `# Copyright 2019 Google LLC
 class Kpt < Formula
   desc "Toolkit to manage,and apply Kubernetes Resource config data files"
   homepage "https://googlecontainertools.github.io/kpt"
-  url "https://github.com/GoogleContainerTools/kpt/archive/{{.Version}}.tar.gz"
-  sha256 "{{.Sha}}"
+  url "{{url}}"
+  sha256 "{{sha256}}"
 
   depends_on "go" => :build
 


Test patch:
diff --git a/internal/testutil/pkgbuilder/builder.go b/internal/testutil/pkgbuilder/builder.go
index ac959686..2495d4fd 100644
--- a/internal/testutil/pkgbuilder/builder.go
+++ b/internal/testutil/pkgbuilder/builder.go
@@ -15,14 +15,12 @@
 package pkgbuilder
 
 import (
-	"bytes"
 	"fmt"
 	"os"
 	"path/filepath"
 	"regexp"
 	"strconv"
 	"testing"
-	"text/template"
 
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
@@ -561,53 +559,6 @@ func buildRGFile(pkg *pkg) string {
 	return string(b)
 }
 
-// TODO: Consider using the Kptfile struct for this instead of a template.
-var kptfileTemplate = `apiVersion: kpt.dev/v1
-kind: Kptfile
-metadata:
-  name: {{.PkgName}}
-{{- if .Pkg.Kptfile.Upstream }}
-upstream:
-  type: git
-  git:
-    repo: {{.Pkg.Kptfile.Upstream.Repo}}
-    directory: {{.Pkg.Kptfile.Upstream.Dir}}
-    ref: {{.Pkg.Kptfile.Upstream.Ref}}
-  updateStrategy: {{.Pkg.Kptfile.Upstream.Strategy}}
-{{- end }}
-{{- if .Pkg.Kptfile.UpstreamLock }}
-upstreamLock:
-  type: git
-  git:
-    repo: {{.Pkg.Kptfile.UpstreamLock.Repo}}
-    directory: {{.Pkg.Kptfile.UpstreamLock.Dir}}
-    ref: {{.Pkg.Kptfile.UpstreamLock.Ref}}
-    commit: {{.Pkg.Kptfile.UpstreamLock.Commit}}
-{{- end }}
-{{- if .Pkg.Kptfile.Pipeline }}
-pipeline:
-  mutators:
-{{- range .Pkg.Kptfile.Pipeline.Functions }}
-  - image: {{ .Image }}
-{{- if .ConfigPath }}
-  - configPath: {{ .ConfigPath }}
-{{- end }}
-{{- end }}
-{{- end }}
-{{- if .Pkg.Kptfile.Inventory }}
-inventory:
-{{- if .Pkg.Kptfile.Inventory.Name }}
-  name: {{ .Pkg.Kptfile.Inventory.Name }}
-{{- end }}
-{{- if .Pkg.Kptfile.Inventory.Namespace }}
-  namespace: {{ .Pkg.Kptfile.Inventory.Namespace }}
-{{- end }}
-{{- if .Pkg.Kptfile.Inventory.ID }}
-  inventoryID: {{ .Pkg.Kptfile.Inventory.ID }}
-{{- end }}
-{{- end }}
-`
-
 type ReposInfo interface {
 	ResolveRepoRef(repoRef string) (string, bool)
 	ResolveCommitIndex(repoRef string, index int) (string, bool)
@@ -635,20 +586,62 @@ func buildKptfile(pkg *pkg, pkgName string, reposInfo ReposInfo) string {
 			pkg.Kptfile.UpstreamLock.Ref = newRef
 		}
 	}
-	tmpl, err := template.New("test").Parse(kptfileTemplate)
-	if err != nil {
-		panic(err)
+
+	kptfile := &kptfilev1.KptFile{}
+	kptfile.APIVersion, kptfile.Kind = kptfilev1.KptFileGVK().ToAPIVersionAndKind()
+	kptfile.ObjectMeta.Name = pkgName
+	if pkg.Kptfile.Upstream != nil {
+		kptfile.Upstream = &kptfilev1.Upstream{
+			Type: "git",
+			Git: &kptfilev1.Git{
+				Repo:      pkg.Kptfile.Upstream.Repo,
+				Directory: pkg.Kptfile.Upstream.Dir,
+				Ref:       pkg.Kptfile.Upstream.Ref,
+			},
+			UpdateStrategy: kptfilev1.UpdateStrategyType(pkg.Kptfile.Upstream.Strategy),
+		}
 	}
-	var buf bytes.Buffer
-	err = tmpl.Execute(&buf, map[string]interface{}{
-		"Pkg":     pkg,
-		"PkgName": pkgName,
-	})
+	if pkg.Kptfile.UpstreamLock != nil {
+		kptfile.UpstreamLock = &kptfilev1.UpstreamLock{
+			Type: "git",
+			Git: &kptfilev1.GitLock{
+				Repo:      pkg.Kptfile.UpstreamLock.Repo,
+				Directory: pkg.Kptfile.UpstreamLock.Dir,
+				Ref:       pkg.Kptfile.UpstreamLock.Ref,
+				Commit:    pkg.Kptfile.UpstreamLock.Commit,
+			},
+		}
+	}
+	if pkg.Kptfile.Pipeline != nil {
+		kptfile.Pipeline = &kptfilev1.Pipeline{}
+		for _, fn := range pkg.Kptfile.Pipeline.Functions {
+			mutator := kptfilev1.Function{
+				Image: fn.Image,
+			}
+			if fn.ConfigPath != "" {
+				mutator.ConfigPath = fn.ConfigPath
+			}
+			kptfile.Pipeline.Mutators = append(kptfile.Pipeline.Mutators, mutator)
+		}
+	}
+
+	if inventory := pkg.Kptfile.Inventory; inventory != nil {
+		kptfile.Inventory = &kptfilev1.Inventory{}
+		if inventory.Name != "" {
+			kptfile.Inventory.Name = inventory.Name
+		}
+		if inventory.Namespace != "" {
+			kptfile.Inventory.Namespace = inventory.Namespace
+		}
+		if inventory.ID != "" {
+			kptfile.Inventory.InventoryID = inventory.ID
+		}
+	}
+	b, err := yaml.Marshal(kptfile)
 	if err != nil {
 		panic(err)
 	}
-	result := buf.String()
-	return result
+	return string(b)
 }
 
 // resolveRepoRef looks up the repo path for a repo from the reposInfo
diff --git a/internal/testutil/pkgbuilder/builder_test.go b/internal/testutil/pkgbuilder/builder_test.go
new file mode 100644
index 00000000..7e7e0ae8
--- /dev/null
+++ b/internal/testutil/pkgbuilder/builder_test.go
@@ -0,0 +1,37 @@
+package pkgbuilder
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestBuildKptfile(t *testing.T) {
+	var repos ReposInfo
+	pkg := &pkg{}
+	pkg.Kptfile = &Kptfile{
+		Pipeline: &Pipeline{
+			Functions: []Function{
+				{Image: "example.com/fn1"},
+				{ConfigPath: "config1"},
+				{Image: "example.com/fn2"},
+			},
+		},
+	}
+
+	got := buildKptfile(pkg, "test1", repos)
+	want := `apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: test1
+pipeline:
+  mutators:
+  - image: example.com/fn1
+  - configPath: config1
+  - image: example.com/fn2
+`
+
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("buildKptfile returned unexpected diff (-want +got):\n%s", diff)
+	}
+}
diff --git a/release/formula/main_test.go b/release/formula/main_test.go
new file mode 100644
index 00000000..7ea62ebb
--- /dev/null
+++ b/release/formula/main_test.go
@@ -0,0 +1,87 @@
+// Copyright 2022 Google LLC
+//
+// 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 main
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestFormula(t *testing.T) {
+	want := `# Copyright 2019 Google LLC
+#
+# 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.
+
+class Kpt < Formula
+  desc "Toolkit to manage,and apply Kubernetes Resource config data files"
+  homepage "https://googlecontainertools.github.io/kpt"
+  url "https://github.com/GoogleContainerTools/kpt/archive/v0.0.0-fake.tar.gz"
+  sha256 "4e42c5ce1a23511405beb5f51cfe07885fa953db448265fe74ee9b81e0def277"
+
+  depends_on "go" => :build
+
+  def install
+    ENV["GO111MODULE"] = "on"
+    system "go", "build", "-ldflags", "-X github.com/GoogleContainerTools/kpt/run.version=#{version}", *std_go_args
+  end
+
+  test do
+    assert_match version.to_s, shell_output("#{bin}/kpt version")
+  end
+end
+`
+
+	httpClient := &http.Client{
+		Transport: &fakeServer{t: t},
+	}
+	url := "https://github.com/GoogleContainerTools/kpt/archive/v0.0.0-fake.tar.gz"
+	got, err := buildFormula(httpClient, url)
+	if err != nil {
+		t.Fatalf("error from buildFormula(%q): %v", url, err)
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("buildFormula(%q) returned unexpected diff (-want +got):\n%s", url, diff)
+	}
+}
+
+type fakeServer struct {
+	t *testing.T
+}
+
+func (s *fakeServer) RoundTrip(req *http.Request) (*http.Response, error) {
+	if req.URL.Path != "/GoogleContainerTools/kpt/archive/v0.0.0-fake.tar.gz" {
+		s.t.Errorf("Expected to request '/GoogleContainerTools/kpt/archive/v0.0.0-fake.tar.gz', got: %s", req.URL.Path)
+	}
+	body := bytes.NewReader([]byte(`This is fake content so that our tests don't download big files`))
+	return &http.Response{
+		Status:     http.StatusText(http.StatusOK),
+		StatusCode: http.StatusOK,
+		Body:       io.NopCloser(body),
+	}, nil
+}