diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7f4b765c8..88e9a3e650 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: # Upload the contents of the build directory for later stages to use - name: Upload build artifacts - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: build-artifacts path: build/ @@ -186,7 +186,7 @@ jobs: HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.brew-tap-token.outputs.token }} - name: Save CVE report - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: cve-report path: build/zarf-known-cves.csv diff --git a/.github/workflows/scan-codeql.yml b/.github/workflows/scan-codeql.yml index e5ab9bd6d8..486fdaa8f8 100644 --- a/.github/workflows/scan-codeql.yml +++ b/.github/workflows/scan-codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: languages: ${{ matrix.language }} config-file: ./.github/codeql.yaml @@ -62,6 +62,6 @@ jobs: run: make build-cli-linux-amd - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index e4c9399538..c1a7d1915c 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -36,7 +36,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: SARIF file path: results.sarif @@ -44,6 +44,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: sarif_file: results.sarif diff --git a/.github/workflows/test-bigbang.yml b/.github/workflows/test-bigbang.yml index 48d2f795cf..6b78a21c4c 100644 --- a/.github/workflows/test-bigbang.yml +++ b/.github/workflows/test-bigbang.yml @@ -63,7 +63,7 @@ jobs: # Upload the contents of the build directory for later stages to use - name: Upload build artifacts - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: build-artifacts path: build/ diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 51faff752d..bdcc0d0c6b 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -47,7 +47,7 @@ jobs: # Upload the contents of the build directory for later stages to use - name: Upload build artifacts - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: build-artifacts path: build/ diff --git a/.github/workflows/test-upgrade.yml b/.github/workflows/test-upgrade.yml index a3230eb6a7..72aa9dfa90 100644 --- a/.github/workflows/test-upgrade.yml +++ b/.github/workflows/test-upgrade.yml @@ -46,7 +46,7 @@ jobs: # Upload the contents of the build directory for later stages to use - name: Upload build artifacts - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: build-artifacts path: build/ diff --git a/src/api/go.mod b/src/api/go.mod index 5aa1a80d28..1351380609 100644 --- a/src/api/go.mod +++ b/src/api/go.mod @@ -5,16 +5,17 @@ go 1.22.4 replace github.com/zarf-dev/zarf => ../.. require ( + github.com/defenseunicorns/pkg/helpers/v2 v2.0.1 github.com/invopop/jsonschema v0.12.0 github.com/stretchr/testify v1.9.0 github.com/zarf-dev/zarf v0.37.0 + k8s.io/apimachinery v0.30.3 ) require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/defenseunicorns/pkg/helpers/v2 v2.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/otiai10/copy v1.14.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -22,5 +23,6 @@ require ( golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect oras.land/oras-go/v2 v2.5.0 // indirect ) diff --git a/src/api/go.sum b/src/api/go.sum index dd52f17424..3cd2d742fb 100644 --- a/src/api/go.sum +++ b/src/api/go.sum @@ -29,5 +29,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= +k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= diff --git a/src/api/v1alpha1/package.go b/src/api/v1alpha1/package.go index 02bfd719c9..f82ccdeb4c 100644 --- a/src/api/v1alpha1/package.go +++ b/src/api/v1alpha1/package.go @@ -9,14 +9,6 @@ import ( "github.com/zarf-dev/zarf/src/pkg/variables" ) -// Zarf looks for these strings in zarf.yaml to make dynamic changes -const ( - ZarfPackageTemplatePrefix = "###ZARF_PKG_TMPL_" - ZarfPackageVariablePrefix = "###ZARF_PKG_VAR_" - ZarfPackageArch = "###ZARF_PKG_ARCH###" - ZarfComponentName = "###ZARF_COMPONENT_NAME###" -) - // ZarfPackageKind is an enum of the different kinds of Zarf packages. type ZarfPackageKind string @@ -25,6 +17,15 @@ const ( ZarfInitConfig ZarfPackageKind = "ZarfInitConfig" // ZarfPackageConfig is the default kind of Zarf package, primarily used during `zarf package`. ZarfPackageConfig ZarfPackageKind = "ZarfPackageConfig" + ApiVersion string = "zarf.dev/v1alpha1" +) + +// Zarf looks for these strings in zarf.yaml to make dynamic changes +const ( + ZarfPackageTemplatePrefix = "###ZARF_PKG_TMPL_" + ZarfPackageVariablePrefix = "###ZARF_PKG_VAR_" + ZarfPackageArch = "###ZARF_PKG_ARCH###" + ZarfComponentName = "###ZARF_COMPONENT_NAME###" ) const apiVersion = "zarf.dev/v1alpha1" @@ -32,7 +33,7 @@ const apiVersion = "zarf.dev/v1alpha1" // ZarfPackage the top-level structure of a Zarf config file. type ZarfPackage struct { // The API version of the Zarf package. - ApiVersion string `json:"apiVersion,omitempty"` + ApiVersion string `json:"apiVersion,omitempty," jsonschema:"enum=zarf.dev/v1alpha1"` // The kind of Zarf package. Kind ZarfPackageKind `json:"kind"` // Package metadata. diff --git a/src/cmd/connect.go b/src/cmd/connect.go index adb9b45df3..8600d703a3 100644 --- a/src/cmd/connect.go +++ b/src/cmd/connect.go @@ -70,7 +70,7 @@ var connectCmd = &cobra.Command{ spinner.Updatef(lang.CmdConnectEstablishedWeb, tunnel.FullURL()) if err := exec.LaunchURL(tunnel.FullURL()); err != nil { - message.Debug(err) + return err } } diff --git a/src/cmd/internal.go b/src/cmd/internal.go index ddc09aad13..4a393d01e6 100644 --- a/src/cmd/internal.go +++ b/src/cmd/internal.go @@ -43,7 +43,11 @@ var agentCmd = &cobra.Command{ Short: lang.CmdInternalAgentShort, Long: lang.CmdInternalAgentLong, RunE: func(cmd *cobra.Command, _ []string) error { - return agent.StartWebhook(cmd.Context()) + cluster, err := cluster.NewCluster() + if err != nil { + return err + } + return agent.StartWebhook(cmd.Context(), cluster) }, } @@ -52,7 +56,11 @@ var httpProxyCmd = &cobra.Command{ Short: lang.CmdInternalProxyShort, Long: lang.CmdInternalProxyLong, RunE: func(cmd *cobra.Command, _ []string) error { - return agent.StartHTTPProxy(cmd.Context()) + cluster, err := cluster.NewCluster() + if err != nil { + return err + } + return agent.StartHTTPProxy(cmd.Context(), cluster) }, } @@ -161,8 +169,8 @@ tableOfContents: false func addGoComments(reflector *jsonschema.Reflector) error { addCommentErr := errors.New("this command must be called from the root of the Zarf repo") - // typePackagePath := filepath.Join("src", "types") - if err := reflector.AddGoComments("github.com/zarf-dev/zarf", "./src/api/v1alpha1"); err != nil { + typePackagePath := filepath.Join("src", "api", "v1alpha1") + if err := reflector.AddGoComments("github.com/zarf-dev/zarf", typePackagePath); err != nil { return fmt.Errorf("%w: %w", addCommentErr, err) } varPackagePath := filepath.Join("src", "pkg", "variables") diff --git a/src/config/lang/english.go b/src/config/lang/english.go index e4057e00bd..8e821b03f2 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -611,7 +611,6 @@ const ( AgentErrMarshallJSONPatch = "unable to marshall the json patch" AgentErrMarshalResponse = "unable to marshal the response" AgentErrNilReq = "malformed admission review: request is nil" - AgentErrUnableTransform = "unable to transform the provided request; see zarf http proxy logs for more details" ) // Package create diff --git a/src/internal/agent/http/proxy.go b/src/internal/agent/http/proxy.go index 33709dfff7..760ba709ec 100644 --- a/src/internal/agent/http/proxy.go +++ b/src/internal/agent/http/proxy.go @@ -5,7 +5,6 @@ package http import ( - "context" "crypto/tls" "fmt" "io" @@ -14,51 +13,43 @@ import ( "net/url" "strings" - "github.com/zarf-dev/zarf/src/config/lang" "github.com/zarf-dev/zarf/src/pkg/cluster" "github.com/zarf-dev/zarf/src/pkg/message" "github.com/zarf-dev/zarf/src/pkg/transform" + "github.com/zarf-dev/zarf/src/types" ) // ProxyHandler constructs a new httputil.ReverseProxy and returns an http handler. -func ProxyHandler() http.HandlerFunc { +func ProxyHandler(cluster *cluster.Cluster) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - err := proxyRequestTransform(r) + state, err := cluster.LoadZarfState(r.Context()) if err != nil { message.Debugf("%#v", err) w.WriteHeader(http.StatusInternalServerError) //nolint: errcheck // ignore - w.Write([]byte(lang.AgentErrUnableTransform)) + w.Write([]byte("unable to load Zarf state, see the Zarf HTTP proxy logs for more details")) + return + } + err = proxyRequestTransform(r, state) + if err != nil { + message.Debugf("%#v", err) + w.WriteHeader(http.StatusInternalServerError) + //nolint: errcheck // ignore + w.Write([]byte("unable to transform the provided request, see the Zarf HTTP proxy logs for more details")) return } - proxy := &httputil.ReverseProxy{Director: func(_ *http.Request) {}, ModifyResponse: proxyResponseTransform} proxy.ServeHTTP(w, r) } } -func proxyRequestTransform(r *http.Request) error { - message.Debugf("Before Req %#v", r) - message.Debugf("Before Req URL %#v", r.URL) - +func proxyRequestTransform(r *http.Request, state *types.ZarfState) error { // We add this so that we can use it to rewrite urls in the response if needed r.Header.Add("X-Forwarded-Host", r.Host) // We remove this so that go will encode and decode on our behalf (see https://pkg.go.dev/net/http#Transport DisableCompression) r.Header.Del("Accept-Encoding") - c, err := cluster.NewCluster() - if err != nil { - return err - } - ctx := context.Background() - state, err := c.LoadZarfState(ctx) - if err != nil { - return err - } - - var targetURL *url.URL - // Setup authentication for each type of service based on User Agent switch { case isGitUserAgent(r.UserAgent()): @@ -70,6 +61,8 @@ func proxyRequestTransform(r *http.Request) error { } // Transform the URL; if we see the NoTransform prefix, strip it; otherwise, transform the URL based on User Agent + var err error + var targetURL *url.URL if strings.HasPrefix(r.URL.Path, transform.NoTransform) { switch { case isGitUserAgent(r.UserAgent()): @@ -89,7 +82,6 @@ func proxyRequestTransform(r *http.Request) error { targetURL, err = transform.GenTransformURL(state.ArtifactServer.Address, getTLSScheme(r.TLS)+r.Host+r.URL.String()) } } - if err != nil { return err } @@ -98,19 +90,12 @@ func proxyRequestTransform(r *http.Request) error { r.URL = targetURL r.RequestURI = getRequestURI(targetURL.Path, targetURL.RawQuery, targetURL.Fragment) - message.Debugf("After Req %#v", r) - message.Debugf("After Req URL%#v", r.URL) - return nil } func proxyResponseTransform(resp *http.Response) error { - message.Debugf("Before Resp %#v", resp) - // Handle redirection codes (3xx) by adding a marker to let Zarf know this has been redirected if resp.StatusCode/100 == 3 { - message.Debugf("Before Resp Location %#v", resp.Header.Get("Location")) - locationURL, err := url.Parse(resp.Header.Get("Location")) if err != nil { return err @@ -119,72 +104,46 @@ func proxyResponseTransform(resp *http.Response) error { locationURL.Host = resp.Request.Header.Get("X-Forwarded-Host") resp.Header.Set("Location", locationURL.String()) - - message.Debugf("After Resp Location %#v", resp.Header.Get("Location")) } - contentType := resp.Header.Get("Content-Type") - // Handle text content returns that may contain links + contentType := resp.Header.Get("Content-Type") if strings.HasPrefix(contentType, "text") || strings.HasPrefix(contentType, "application/json") || strings.HasPrefix(contentType, "application/xml") { - err := replaceBodyLinks(resp) - + forwardedPrefix := fmt.Sprintf("%s%s%s", getTLSScheme(resp.Request.TLS), resp.Request.Header.Get("X-Forwarded-Host"), transform.NoTransform) + targetPrefix := fmt.Sprintf("%s%s", getTLSScheme(resp.TLS), resp.Request.Host) + b, err := io.ReadAll(resp.Body) if err != nil { - message.Debugf("%#v", err) + return err } - } - - message.Debugf("After Resp %#v", resp) - - return nil -} - -func replaceBodyLinks(resp *http.Response) error { - message.Debugf("Resp Request: %#v", resp.Request) - - // Create the forwarded (online) and target (offline) URL prefixes to replace - forwardedPrefix := fmt.Sprintf("%s%s%s", getTLSScheme(resp.Request.TLS), resp.Request.Header.Get("X-Forwarded-Host"), transform.NoTransform) - targetPrefix := fmt.Sprintf("%s%s", getTLSScheme(resp.TLS), resp.Request.Host) + err = resp.Body.Close() + if err != nil { + return err + } + bodyString := strings.ReplaceAll(string(b), targetPrefix, forwardedPrefix) - b, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - err = resp.Body.Close() - if err != nil { - return err + resp.Body = io.NopCloser(strings.NewReader(bodyString)) + resp.ContentLength = int64(len(bodyString)) + resp.Header.Set("Content-Length", fmt.Sprint(int64(len(bodyString)))) } - bodyString := strings.ReplaceAll(string(b), targetPrefix, forwardedPrefix) - - // Setup the new reader, and correct the content length - resp.Body = io.NopCloser(strings.NewReader(bodyString)) - resp.ContentLength = int64(len(bodyString)) - resp.Header.Set("Content-Length", fmt.Sprint(int64(len(bodyString)))) - return nil } func getTLSScheme(tls *tls.ConnectionState) string { scheme := "https://" - if tls == nil { scheme = "http://" } - return scheme } func getRequestURI(path, query, fragment string) string { uri := path - if query != "" { uri += "?" + query } - if fragment != "" { uri += "#" + fragment } - return uri } diff --git a/src/internal/agent/http/proxy_test.go b/src/internal/agent/http/proxy_test.go new file mode 100644 index 0000000000..c5883d25b4 --- /dev/null +++ b/src/internal/agent/http/proxy_test.go @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package http + +import ( + "crypto/tls" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/types" +) + +func TestProxyRequestTransform(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + target string + state *types.ZarfState + expectedPath string + }{ + { + name: "basic request", + target: "http://example.com/zarf-3xx-no-transform/test", + state: &types.ZarfState{ + ArtifactServer: types.ArtifactServerInfo{ + PushUsername: "push-user", + PushToken: "push-token", + }, + }, + expectedPath: "/test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest(http.MethodGet, tt.target, nil) + req.Header.Set("Accept-Encoding", "foo") + err := proxyRequestTransform(req, tt.state) + require.NoError(t, err) + + require.Empty(t, req.Header.Get("Accept-Encoding")) + + username, password, ok := req.BasicAuth() + require.True(t, ok) + require.Equal(t, tt.state.ArtifactServer.PushUsername, username) + require.Equal(t, tt.state.ArtifactServer.PushToken, password) + + require.Equal(t, tt.expectedPath, req.URL.Path) + }) + } +} + +func TestGetTLSScheme(t *testing.T) { + t.Parallel() + + scheme := getTLSScheme(nil) + require.Equal(t, "http://", scheme) + scheme = getTLSScheme(&tls.ConnectionState{}) + require.Equal(t, "https://", scheme) +} + +func TestGetRequestURI(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + path string + query string + fragment string + expected string + }{ + { + name: "basic", + path: "/foo", + query: "", + fragment: "", + expected: "/foo", + }, + { + name: "query", + path: "/foo", + query: "key=value", + fragment: "", + expected: "/foo?key=value", + }, + { + name: "fragment", + path: "/foo", + query: "", + fragment: "bar", + expected: "/foo#bar", + }, + { + name: "query and fragment", + path: "/foo", + query: "key=value", + fragment: "bar", + expected: "/foo?key=value#bar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + uri := getRequestURI(tt.path, tt.query, tt.fragment) + require.Equal(t, tt.expected, uri) + }) + } +} + +func TestUserAgent(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + userAgent string + expectedGit bool + expectedPip bool + expectedNpm bool + }{ + { + name: "unknown user agent", + userAgent: "Firefox", + expectedGit: false, + expectedPip: false, + expectedNpm: false, + }, + { + name: "git user agent", + userAgent: "git/2.0.0", + expectedGit: true, + expectedPip: false, + expectedNpm: false, + }, + { + name: "pip user agent", + userAgent: "pip/1.2.3", + expectedGit: false, + expectedPip: true, + expectedNpm: false, + }, + { + name: "twine user agent", + userAgent: "twine/1.8.1", + expectedGit: false, + expectedPip: true, + expectedNpm: false, + }, + { + name: "npm user agent", + userAgent: "npm/1.0.0", + expectedGit: false, + expectedPip: false, + expectedNpm: true, + }, + { + name: "pnpm user agent", + userAgent: "pnpm/1.0.0", + expectedGit: false, + expectedPip: false, + expectedNpm: true, + }, + { + name: "yarn user agent", + userAgent: "yarn/1.0.0", + expectedGit: false, + expectedPip: false, + expectedNpm: true, + }, + { + name: "bun user agent", + userAgent: "bun/1.0.0", + expectedGit: false, + expectedPip: false, + expectedNpm: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tt.expectedGit, isGitUserAgent(tt.userAgent)) + require.Equal(t, tt.expectedPip, isPipUserAgent(tt.userAgent)) + require.Equal(t, tt.expectedNpm, isNpmUserAgent(tt.userAgent)) + }) + } +} diff --git a/src/internal/agent/http/server.go b/src/internal/agent/http/server.go deleted file mode 100644 index 6a79aaa449..0000000000 --- a/src/internal/agent/http/server.go +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package http provides a http server for the webhook and proxy. -package http - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/zarf-dev/zarf/src/internal/agent/hooks" - "github.com/zarf-dev/zarf/src/internal/agent/http/admission" - "github.com/zarf-dev/zarf/src/pkg/cluster" -) - -// NewAdmissionServer creates a http.Server for the mutating webhook admission handler. -func NewAdmissionServer(ctx context.Context, port string) (*http.Server, error) { - c, err := cluster.NewCluster() - if err != nil { - return nil, err - } - - // Routers - admissionHandler := admission.NewHandler() - podsMutation := hooks.NewPodMutationHook(ctx, c) - fluxGitRepositoryMutation := hooks.NewGitRepositoryMutationHook(ctx, c) - argocdApplicationMutation := hooks.NewApplicationMutationHook(ctx, c) - argocdRepositoryMutation := hooks.NewRepositorySecretMutationHook(ctx, c) - fluxHelmRepositoryMutation := hooks.NewHelmRepositoryMutationHook(ctx, c) - fluxOCIRepositoryMutation := hooks.NewOCIRepositoryMutationHook(ctx, c) - - // Routers - mux := http.NewServeMux() - mux.Handle("/healthz", healthz()) - mux.Handle("/mutate/pod", admissionHandler.Serve(podsMutation)) - mux.Handle("/mutate/flux-gitrepository", admissionHandler.Serve(fluxGitRepositoryMutation)) - mux.Handle("/mutate/flux-helmrepository", admissionHandler.Serve(fluxHelmRepositoryMutation)) - mux.Handle("/mutate/flux-ocirepository", admissionHandler.Serve(fluxOCIRepositoryMutation)) - mux.Handle("/mutate/argocd-application", admissionHandler.Serve(argocdApplicationMutation)) - mux.Handle("/mutate/argocd-repository", admissionHandler.Serve(argocdRepositoryMutation)) - mux.Handle("/metrics", promhttp.Handler()) - - srv := &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, // Set ReadHeaderTimeout to avoid Slowloris attacks - } - return srv, nil -} - -// NewProxyServer creates and returns an http proxy server. -func NewProxyServer(port string) *http.Server { - mux := http.NewServeMux() - mux.Handle("/healthz", healthz()) - mux.Handle("/", ProxyHandler()) - mux.Handle("/metrics", promhttp.Handler()) - - return &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, // Set ReadHeaderTimeout to avoid Slowloris attacks - } -} - -func healthz() http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - //nolint: errcheck // ignore - w.Write([]byte("ok")) - } -} diff --git a/src/internal/agent/start.go b/src/internal/agent/start.go index 80a04c642f..e620c3648a 100644 --- a/src/internal/agent/start.go +++ b/src/internal/agent/start.go @@ -7,13 +7,18 @@ package agent import ( "context" "errors" + "fmt" "net/http" "time" + "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/sync/errgroup" "github.com/zarf-dev/zarf/src/config/lang" + "github.com/zarf-dev/zarf/src/internal/agent/hooks" agentHttp "github.com/zarf-dev/zarf/src/internal/agent/http" + "github.com/zarf-dev/zarf/src/internal/agent/http/admission" + "github.com/zarf-dev/zarf/src/pkg/cluster" "github.com/zarf-dev/zarf/src/pkg/message" ) @@ -28,20 +33,48 @@ const ( ) // StartWebhook launches the Zarf agent mutating webhook in the cluster. -func StartWebhook(ctx context.Context) error { - srv, err := agentHttp.NewAdmissionServer(ctx, httpPort) - if err != nil { - return err - } - return startServer(ctx, srv) +func StartWebhook(ctx context.Context, cluster *cluster.Cluster) error { + // Routers + admissionHandler := admission.NewHandler() + podsMutation := hooks.NewPodMutationHook(ctx, cluster) + fluxGitRepositoryMutation := hooks.NewGitRepositoryMutationHook(ctx, cluster) + argocdApplicationMutation := hooks.NewApplicationMutationHook(ctx, cluster) + argocdRepositoryMutation := hooks.NewRepositorySecretMutationHook(ctx, cluster) + fluxHelmRepositoryMutation := hooks.NewHelmRepositoryMutationHook(ctx, cluster) + fluxOCIRepositoryMutation := hooks.NewOCIRepositoryMutationHook(ctx, cluster) + + // Routers + mux := http.NewServeMux() + mux.Handle("/mutate/pod", admissionHandler.Serve(podsMutation)) + mux.Handle("/mutate/flux-gitrepository", admissionHandler.Serve(fluxGitRepositoryMutation)) + mux.Handle("/mutate/flux-helmrepository", admissionHandler.Serve(fluxHelmRepositoryMutation)) + mux.Handle("/mutate/flux-ocirepository", admissionHandler.Serve(fluxOCIRepositoryMutation)) + mux.Handle("/mutate/argocd-application", admissionHandler.Serve(argocdApplicationMutation)) + mux.Handle("/mutate/argocd-repository", admissionHandler.Serve(argocdRepositoryMutation)) + + return startServer(ctx, httpPort, mux) } // StartHTTPProxy launches the zarf agent proxy in the cluster. -func StartHTTPProxy(ctx context.Context) error { - return startServer(ctx, agentHttp.NewProxyServer(httpPort)) +func StartHTTPProxy(ctx context.Context, cluster *cluster.Cluster) error { + mux := http.NewServeMux() + mux.Handle("/", agentHttp.ProxyHandler(cluster)) + return startServer(ctx, httpPort, mux) } -func startServer(ctx context.Context, srv *http.Server) error { +func startServer(ctx context.Context, port string, mux *http.ServeMux) error { + mux.Handle("/metrics", promhttp.Handler()) + mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + //nolint: errcheck // ignore + w.Write([]byte("ok")) + })) + srv := &http.Server{ + Addr: fmt.Sprintf(":%s", port), + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, // Set ReadHeaderTimeout to avoid Slowloris attacks + } + g, gCtx := errgroup.WithContext(ctx) g.Go(func() error { err := srv.ListenAndServeTLS(tlsCert, tlsKey) diff --git a/src/pkg/cluster/zarf.go b/src/pkg/cluster/zarf.go index cf14ab2f83..6172e3fa78 100644 --- a/src/pkg/cluster/zarf.go +++ b/src/pkg/cluster/zarf.go @@ -55,14 +55,17 @@ func (c *Cluster) GetDeployedZarfPackages(ctx context.Context) ([]types.Deployed // GetDeployedPackage gets the metadata information about the package name provided (if it exists in the cluster). // We determine what packages have been deployed to the cluster by looking for specific secrets in the Zarf namespace. -func (c *Cluster) GetDeployedPackage(ctx context.Context, packageName string) (deployedPackage *types.DeployedPackage, err error) { - // Get the secret that describes the deployed package +func (c *Cluster) GetDeployedPackage(ctx context.Context, packageName string) (*types.DeployedPackage, error) { secret, err := c.Clientset.CoreV1().Secrets(ZarfNamespaceName).Get(ctx, config.ZarfPackagePrefix+packageName, metav1.GetOptions{}) if err != nil { - return deployedPackage, err + return nil, err } - - return deployedPackage, json.Unmarshal(secret.Data["data"], &deployedPackage) + deployedPackage := &types.DeployedPackage{} + err = json.Unmarshal(secret.Data["data"], deployedPackage) + if err != nil { + return nil, err + } + return deployedPackage, nil } // StripZarfLabelsAndSecretsFromNamespaces removes metadata and secrets from existing namespaces no longer manged by Zarf. diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index dd4588a193..6a3ec004e7 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -38,7 +38,6 @@ type Packager struct { hpaModified bool connectStrings types.ConnectStrings source sources.PackageSource - generation int } // Modifier is a function that modifies the packager. @@ -155,12 +154,6 @@ func (p *Packager) attemptClusterChecks(ctx context.Context) (err error) { spinner := message.NewProgressSpinner("Gathering additional cluster information (if available)") defer spinner.Stop() - // Check if the package has already been deployed and get its generation - if existingDeployedPackage, _ := p.cluster.GetDeployedPackage(ctx, p.cfg.Pkg.Metadata.Name); existingDeployedPackage != nil { - // If this package has been deployed before, increment the package generation within the secret - p.generation = existingDeployedPackage.Generation + 1 - } - // Check the clusters architecture matches the package spec if err := p.validatePackageArchitecture(ctx); err != nil { if errors.Is(err, lang.ErrUnableToCheckArch) { diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 7088947cd8..86c2f43349 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -139,20 +139,10 @@ func (p *Packager) Deploy(ctx context.Context) error { // deployComponents loops through a list of ZarfComponents and deploys them. func (p *Packager) deployComponents(ctx context.Context) (deployedComponents []types.DeployedComponent, err error) { - // Check if this package has been deployed before and grab relevant information about already deployed components - if p.generation == 0 { - p.generation = 1 // If this is the first deployment, set the generation to 1 - } - // Process all the components we are deploying for _, component := range p.cfg.Pkg.Components { - deployedComponent := types.DeployedComponent{ - Name: component.Name, - Status: types.ComponentStatusDeploying, - ObservedGeneration: p.generation, - } - - // If this component requires a cluster, connect to one + // Connect to cluster if a component requires it. + packageGeneration := 1 if component.RequiresCluster() { timeout := cluster.DefaultTimeout if p.cfg.Pkg.IsInitConfig() { @@ -161,10 +151,21 @@ func (p *Packager) deployComponents(ctx context.Context) (deployedComponents []t connectCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() if err := p.connectToCluster(connectCtx); err != nil { - return deployedComponents, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) + return nil, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) + } + + // If this package has been deployed before, increment the package generation within the secret + if existingDeployedPackage, _ := p.cluster.GetDeployedPackage(ctx, p.cfg.Pkg.Metadata.Name); existingDeployedPackage != nil { + packageGeneration = existingDeployedPackage.Generation + 1 } } + deployedComponent := types.DeployedComponent{ + Name: component.Name, + Status: types.ComponentStatusDeploying, + ObservedGeneration: packageGeneration, + } + // Ensure we don't overwrite any installedCharts data when updating the package secret if p.isConnectedToCluster() { deployedComponent.InstalledCharts, err = p.cluster.GetInstalledChartsForComponent(ctx, p.cfg.Pkg.Metadata.Name, component) @@ -178,7 +179,7 @@ func (p *Packager) deployComponents(ctx context.Context) (deployedComponents []t // Update the package secret to indicate that we are attempting to deploy this component if p.isConnectedToCluster() { - if _, err := p.cluster.RecordPackageDeploymentAndWait(ctx, p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { + if _, err := p.cluster.RecordPackageDeploymentAndWait(ctx, p.cfg.Pkg, deployedComponents, p.connectStrings, packageGeneration, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { message.Debugf("Unable to record package deployment for component %s: this will affect features like `zarf package remove`: %s", component.Name, err.Error()) } } @@ -206,7 +207,7 @@ func (p *Packager) deployComponents(ctx context.Context) (deployedComponents []t // Update the package secret to indicate that we failed to deploy this component deployedComponents[idx].Status = types.ComponentStatusFailed if p.isConnectedToCluster() { - if _, err := p.cluster.RecordPackageDeploymentAndWait(ctx, p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { + if _, err := p.cluster.RecordPackageDeploymentAndWait(ctx, p.cfg.Pkg, deployedComponents, p.connectStrings, packageGeneration, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { message.Debugf("Unable to record package deployment for component %q: this will affect features like `zarf package remove`: %s", component.Name, err.Error()) } } @@ -218,7 +219,7 @@ func (p *Packager) deployComponents(ctx context.Context) (deployedComponents []t deployedComponents[idx].InstalledCharts = charts deployedComponents[idx].Status = types.ComponentStatusSucceeded if p.isConnectedToCluster() { - if _, err := p.cluster.RecordPackageDeploymentAndWait(ctx, p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { + if _, err := p.cluster.RecordPackageDeploymentAndWait(ctx, p.cfg.Pkg, deployedComponents, p.connectStrings, packageGeneration, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { message.Debugf("Unable to record package deployment for component %q: this will affect features like `zarf package remove`: %s", component.Name, err.Error()) } } @@ -408,7 +409,7 @@ func (p *Packager) processComponentFiles(component v1alpha1.ZarfComponent, pkgLo // Check if the file looks like a text file isText, err := helpers.IsTextFile(subFile) if err != nil { - message.Debugf("unable to determine if file %s is a text file: %s", subFile, err) + return err } // If the file is a text file, template it diff --git a/src/pkg/rules/schema_test.go b/src/pkg/rules/schema_test.go index a1ef87314d..0a3a58100f 100644 --- a/src/pkg/rules/schema_test.go +++ b/src/pkg/rules/schema_test.go @@ -28,7 +28,8 @@ func TestZarfSchema(t *testing.T) { { name: "valid package", pkg: v1alpha1.ZarfPackage{ - Kind: v1alpha1.ZarfInitConfig, + ApiVersion: v1alpha1.ApiVersion, + Kind: v1alpha1.ZarfInitConfig, Metadata: v1alpha1.ZarfMetadata{ Name: "valid-name", }, @@ -56,7 +57,8 @@ func TestZarfSchema(t *testing.T) { { name: "invalid package", pkg: v1alpha1.ZarfPackage{ - Kind: v1alpha1.ZarfInitConfig, + ApiVersion: "bad-api-version/wrong", + Kind: v1alpha1.ZarfInitConfig, Metadata: v1alpha1.ZarfMetadata{ Name: "-invalid-name", }, @@ -113,6 +115,7 @@ func TestZarfSchema(t *testing.T) { "components.1.actions.onRemove.onSuccess.0.setVariables.0.name: Does not match pattern '^[A-Z0-9_]+$'", "components.0.import.path: Must not validate the schema (not)", "components.0.import.url: Must not validate the schema (not)", + "apiVersion: apiVersion must be one of the following: \"zarf.dev/v1alpha1\"", }, }, }