diff --git a/Dockerfile.podman-next b/Dockerfile.podman-next index 6698623034..9adab22de6 100644 --- a/Dockerfile.podman-next +++ b/Dockerfile.podman-next @@ -10,7 +10,7 @@ unqualified-search-registries = ["docker.io", "quay.io", "registry.fedoraproject short-name-mode="permissive" [[registry]] -location="localhost:50000" +location="registry.localtest.me" insecure=true EOF diff --git a/cmd/build.go b/cmd/build.go index 51692b11a2..1c16cdce9b 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -448,7 +448,7 @@ func (c buildConfig) clientOptions() ([]fn.Option, error) { } t := newTransport(c.RegistryInsecure) - creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile) + creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile, c.RegistryInsecure) switch c.Builder { case builders.Host: @@ -470,7 +470,8 @@ func (c buildConfig) clientOptions() ([]fn.Option, error) { fn.WithPusher(docker.NewPusher( docker.WithCredentialsProvider(creds), docker.WithTransport(t), - docker.WithVerbose(c.Verbose)))) + docker.WithVerbose(c.Verbose), + docker.WithInsecure(c.RegistryInsecure)))) case builders.S2I: o = append(o, fn.WithScaffolder(s2i.NewScaffolder(c.Verbose)), @@ -480,7 +481,8 @@ func (c buildConfig) clientOptions() ([]fn.Option, error) { fn.WithPusher(docker.NewPusher( docker.WithCredentialsProvider(creds), docker.WithTransport(t), - docker.WithVerbose(c.Verbose)))) + docker.WithVerbose(c.Verbose), + docker.WithInsecure(c.RegistryInsecure)))) default: return o, builders.ErrUnknownBuilder{Name: c.Builder, Known: KnownBuilders()} } diff --git a/cmd/client.go b/cmd/client.go index b83b3fd4fa..218bb187d9 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -59,9 +59,9 @@ func NewTestClient(options ...fn.Option) ClientFactory { // 'Verbose' indicates the system should write out a higher amount of logging. func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { var ( - t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies - c = newCredentialsProvider(config.Dir(), t, "") // for accessing registries - d = newKnativeDeployer(cfg.Verbose) // default deployer (can be overridden via options) + t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies + c = newCredentialsProvider(config.Dir(), t, "", cfg.InsecureSkipVerify) // for accessing registries + d = newKnativeDeployer(cfg.Verbose) // default deployer (can be overridden via options) pp = newTektonPipelinesProvider(c, cfg.Verbose, t) o = []fn.Option{ // standard (shared) options for all commands fn.WithVerbose(cfg.Verbose), @@ -81,7 +81,8 @@ func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { fn.WithPusher(docker.NewPusher( docker.WithCredentialsProvider(c), docker.WithTransport(t), - docker.WithVerbose(cfg.Verbose))), + docker.WithVerbose(cfg.Verbose), + docker.WithInsecure(cfg.InsecureSkipVerify))), } ) @@ -110,7 +111,8 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser { // has cluster-flavor specific additional credential loaders to take advantage // of features or configuration nuances of cluster variants. // If authFilePath is provided (non-empty), it will be used as the primary auth file. -func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string) oci.CredentialsProvider { +// When insecure is true, credential verification uses plain HTTP instead of HTTPS. +func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string, insecure bool) oci.CredentialsProvider { additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(), k8s.GetGoogleCredentialLoader()...) additionalLoaders = append(additionalLoaders, k8s.GetECRCredentialLoader()...) additionalLoaders = append(additionalLoaders, k8s.GetACRCredentialLoader()...) @@ -135,6 +137,7 @@ func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin, os.Stdout, os.Stderr)), creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()), creds.WithTransport(t), + creds.WithInsecure(insecure), creds.WithAdditionalCredentialLoaders(additionalLoaders...), } diff --git a/cmd/deploy.go b/cmd/deploy.go index f7d7284647..faf4f3109b 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -791,7 +791,7 @@ func (c deployConfig) clientOptions() ([]fn.Option, error) { } t := newTransport(c.RegistryInsecure) - creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile) + creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile, c.RegistryInsecure) // Override the pipelines provider to use custom credentials // This is needed for remote builds (deploy --remote) diff --git a/e2e/README.md b/e2e/README.md index c6a02a634e..b4e7f30a92 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -99,13 +99,13 @@ the `kn-func` plugged in use `FUNC_E2E_BIN=/path/to/kn FUNC_E2E_PLUGIN=func`. `FUNC_E2E_REGISTRY`: if provided, tests will use this registry (in form `registry.example.com/user`) instead of the test suite default of -`localhost:50000/func`. This is used for local builds that push from the +`registry.localtest.me/func`. This is used for local builds that push from the developer's machine to the registry. `FUNC_E2E_CLUSTER_REGISTRY`: specifies the cluster-internal registry URL used for in-cluster (remote) builds with Tekton. This registry must be accessible from within the cluster. Format: `registry.namespace.svc.cluster.local:port/path`. -Defaults to `registry.default.svc.cluster.local:5000/func`. +Defaults to `registry.localtest.me/func`. `FUNC_E2E_MATRIX_RUNTIMES`: Sets which runtimes will be tested during the matrix tests. By default matrix test are not enabled unless this value is passed. diff --git a/e2e/e2e_config_ci_test.go b/e2e/e2e_config_ci_test.go index 70b7d4197f..cd1292d965 100644 --- a/e2e/e2e_config_ci_test.go +++ b/e2e/e2e_config_ci_test.go @@ -6,7 +6,11 @@ import ( "fmt" "os" "os/exec" + "path/filepath" + "strings" "testing" + + "gopkg.in/yaml.v3" ) func TestConfigCI_DeployFuncViaGeneratedGitHubWorkflow(t *testing.T) { @@ -72,12 +76,20 @@ func gitInit(t *testing.T, dir string) { func runGitHubWorkflow(t *testing.T, dir string) { t.Helper() - cmd := exec.Command("act", "push", + // Patch the generated workflow to use the locally-built func binary + // instead of downloading a release via functions-dev/action. + patchWorkflowWithLocalBinary(t, filepath.Join(dir, ".github", "workflows", "func-deploy.yaml")) + + args := []string{"push", "-P", "ubuntu-latest=-self-hosted", "-W", ".github/workflows/func-deploy.yaml", - "-s", "KUBECONFIG="+readFile(t, Kubeconfig), - "--var", "REGISTRY_URL="+Registry, - ) + "-s", "KUBECONFIG=" + readFile(t, Kubeconfig), + "--var", "REGISTRY_URL=" + Registry, + } + if strings.Contains(Registry, "registry.localtest.me") { + args = append(args, "--env", "FUNC_REGISTRY_INSECURE=true") + } + cmd := exec.Command("act", args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -86,6 +98,49 @@ func runGitHubWorkflow(t *testing.T, dir string) { } } +// patchWorkflowWithLocalBinary replaces the "Install func cli" step in the +// generated GitHub workflow with a step that symlinks the locally-built binary. +// This ensures the CI config test exercises the current code rather than a +// released version that may lack recent fixes. +func patchWorkflowWithLocalBinary(t *testing.T, workflowPath string) { + t.Helper() + + data, err := os.ReadFile(workflowPath) + if err != nil { + t.Fatalf("failed to read workflow %s: %v", workflowPath, err) + } + + var wf map[string]interface{} + if err := yaml.Unmarshal(data, &wf); err != nil { + t.Fatalf("failed to parse workflow: %v", err) + } + + jobs, _ := wf["jobs"].(map[string]interface{}) + deploy, _ := jobs["deploy"].(map[string]interface{}) + steps, _ := deploy["steps"].([]interface{}) + + binDir := t.TempDir() + for i, s := range steps { + step, _ := s.(map[string]interface{}) + if step["name"] == "Install func cli" { + steps[i] = map[string]interface{}{ + "name": "Install func cli", + "run": fmt.Sprintf("ln -sf %s %s/func && echo %s >> $GITHUB_PATH", Bin, binDir, binDir), + } + break + } + } + + out, err := yaml.Marshal(wf) + if err != nil { + t.Fatalf("failed to marshal workflow: %v", err) + } + + if err := os.WriteFile(workflowPath, out, 0644); err != nil { + t.Fatalf("failed to write workflow %s: %v", workflowPath, err) + } +} + func readFile(t *testing.T, path string) string { t.Helper() diff --git a/e2e/e2e_matrix_test.go b/e2e/e2e_matrix_test.go index 095062ef59..a918237d78 100644 --- a/e2e/e2e_matrix_test.go +++ b/e2e/e2e_matrix_test.go @@ -136,7 +136,7 @@ func TestMatrix_Remote(t *testing.T) { } // Deploy - if err := newCmd(t, "deploy", "--builder", builder, "--remote", fmt.Sprintf("--registry=%s", ClusterRegistry)).Run(); err != nil { + if err := newCmd(t, "deploy", "--builder", builder, "--remote", fmt.Sprintf("--registry=%s", Registry)).Run(); err != nil { t.Fatal(err) } diff --git a/e2e/e2e_remote_test.go b/e2e/e2e_remote_test.go index 20701068d4..301ffbbfc2 100644 --- a/e2e/e2e_remote_test.go +++ b/e2e/e2e_remote_test.go @@ -27,7 +27,7 @@ func TestRemote_Deploy(t *testing.T) { if err := newCmd(t, "init", "-l=go").Run(); err != nil { t.Fatal(err) } - if err := newCmd(t, "deploy", "--remote", "--builder=pack", fmt.Sprintf("--registry=%s", ClusterRegistry)).Run(); err != nil { + if err := newCmd(t, "deploy", "--remote", "--builder=pack", fmt.Sprintf("--registry=%s", Registry)).Run(); err != nil { t.Fatal(err) } defer func() { @@ -57,7 +57,7 @@ func TestRemote_Source(t *testing.T) { // Trigger the deploy if err := newCmd(t, "deploy", "--remote", "--git-url", "https://github.com/functions-dev/func-e2e-tests", - "--registry", ClusterRegistry, + "--registry", Registry, "--builder", "pack", ).Run(); err != nil { t.Fatal(err) @@ -101,7 +101,7 @@ func TestRemote_Ref(t *testing.T) { if err := newCmd(t, "deploy", "--remote", "--git-url", "https://github.com/functions-dev/func-e2e-tests", "--git-branch", name, - "--registry", ClusterRegistry, + "--registry", Registry, "--builder", "pack", "--build", ).Run(); err != nil { @@ -148,7 +148,7 @@ func TestRemote_Dir(t *testing.T) { if err := newCmd(t, "deploy", "--remote", "--git-url", "https://github.com/functions-dev/func-e2e-tests", "--git-dir", name, - "--registry", ClusterRegistry, + "--registry", Registry, "--builder", "pack", "--build", ).Run(); err != nil { @@ -163,3 +163,26 @@ func TestRemote_Dir(t *testing.T) { t.Fatal("function did not deploy correctly") } } + +// TestRemote_Deploy_InClusterRegistry ensures that the in-cluster dialer +// tunneling path works correctly by using the cluster-internal registry URL. +// +// func deploy --remote --registry=registry.default.svc.cluster.local:5000/func +func TestRemote_Deploy_InClusterRegistry(t *testing.T) { + name := "func-e2e-test-remote-incluster" + _ = fromCleanEnv(t, name) + + if err := newCmd(t, "init", "-l=go").Run(); err != nil { + t.Fatal(err) + } + if err := newCmd(t, "deploy", "--remote", "--builder=pack", "--registry=registry.default.svc.cluster.local:5000/func").Run(); err != nil { + t.Fatal(err) + } + defer func() { + clean(t, name, Namespace) + }() + + if !waitFor(t, ksvcUrl(name)) { + t.Fatal("function did not deploy correctly") + } +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 8fbceee74c..abbb2511e5 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -88,13 +88,13 @@ const ( // of the registry created by default when using the cluster.sh script // to set up a local testing cluster, but can be customized with // FUNC_E2E_REGISTRY. - DefaultRegistry = "localhost:50000/func" + DefaultRegistry = "registry.localtest.me/func" // DefaultClusterRegistry to use for in-cluster (remote) builds. // This is the cluster-internal registry URL accessible from within // the cluster for Tekton builds. Can be customized with // FUNC_E2E_CLUSTER_REGISTRY. - DefaultClusterRegistry = "registry.default.svc.cluster.local:5000/func" + DefaultClusterRegistry = "registry.localtest.me/func" // DefaultVerbose sets the default for the --verbose flag of all commands. DefaultVerbose = false @@ -494,6 +494,12 @@ func setupEnv(t *testing.T) { // global config, or already defaulted by the user via environment variable. os.Setenv("FUNC_REGISTRY", Registry) + // When using the default registry (registry.localtest.me), mark it as + // insecure since it serves plain HTTP. + if strings.Contains(Registry, "registry.localtest.me") { + os.Setenv("FUNC_REGISTRY_INSECURE", "true") + } + // If the docker host is set, it should affect any tests which perform // container operations except for podman-specific tests. These use // the FUNC_E2E_PODMAN_HOST value during test execution directly. @@ -507,8 +513,7 @@ func setupEnv(t *testing.T) { } // setupPodmanEnvs -// - configures VM to treat localhost:50000 as an insecure registry -// - proxy connections to the host if running in a VM (like on darwin) +// - configures VM to treat registry.localtest.me as an insecure registry // - creates an XDG_CONFIG_HOME and XDG_DATA_HOME func setupPodman(t *testing.T) error { t.Helper() @@ -523,7 +528,7 @@ func setupPodman(t *testing.T) error { short-name-mode="permissive" [[registry]] -location="localhost:50000" +location="registry.localtest.me" insecure=true ` cfgPath := filepath.Join(t.TempDir(), "registries.conf") @@ -559,24 +564,6 @@ insecure=true return fmt.Errorf("Podman machine is not running. Please start it with: podman machine start podman-machine-default") } - // Kill any existing process on port 50000 in the Podman VM - killCmd := exec.Command("podman", "machine", "ssh", "--", - "sudo lsof -ti :50000 | sudo xargs kill -9 2>/dev/null || true") - if output, err = killCmd.CombinedOutput(); err != nil { - t.Logf("output: %s", output) - return fmt.Errorf("failed killing existing registry proxy: %v", err) - } - - // Set up socat proxy to forward localhost:50000 to host.containers.internal:50000 - // This allows containers in Podman to access the host's registry - proxyCmd := exec.Command("podman", "machine", "ssh", "--", - "sudo sh -c 'socat TCP-LISTEN:50000,fork,reuseaddr TCP:host.containers.internal:50000 /dev/null 2>&1 & echo Registry proxy started'") - if output, err = proxyCmd.CombinedOutput(); err != nil { - t.Logf("output: %s", output) - return fmt.Errorf("failed to set up registry proxy: %v, output: %s", err, output) - } - t.Logf("Podman registry proxy enabled: %s", strings.TrimSpace(string(output))) - return nil } diff --git a/e2e/e2e_userdeps_test.go b/e2e/e2e_userdeps_test.go index 36868c1097..8ee12fea85 100644 --- a/e2e/e2e_userdeps_test.go +++ b/e2e/e2e_userdeps_test.go @@ -57,7 +57,7 @@ func TestCore_PythonUserDepsRemote(t *testing.T) { // Deploy remotely via Tekton if err := newCmd(t, "deploy", "--builder", "pack", "--remote", - fmt.Sprintf("--registry=%s", ClusterRegistry)).Run(); err != nil { + fmt.Sprintf("--registry=%s", Registry)).Run(); err != nil { t.Fatal(err) } diff --git a/hack/allow-insecure.tar b/hack/allow-insecure.tar index 50cae25f58..8bbbcc22a9 100644 Binary files a/hack/allow-insecure.tar and b/hack/allow-insecure.tar differ diff --git a/hack/cluster.sh b/hack/cluster.sh index 9bdbab61cb..e60a087eaa 100755 --- a/hack/cluster.sh +++ b/hack/cluster.sh @@ -123,14 +123,14 @@ nodes: listenAddress: "127.0.0.1" containerdConfigPatches: - |- - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:50000"] - endpoint = ["http://func-registry:5000"] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.localtest.me"] + endpoint = ["http://localhost:5000"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.default.svc.cluster.local:5000"] - endpoint = ["http://func-registry:5000"] + endpoint = ["http://localhost:5000"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"] - endpoint = ["http://func-registry:5000"] + endpoint = ["http://localhost:5000"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"] - endpoint = ["http://func-registry:5000"] + endpoint = ["http://localhost:5000"] EOF sleep 10 $KUBECTL wait pod --for=condition=Ready -l '!job-name' -n kube-system --timeout=5m @@ -320,15 +320,69 @@ EOF } registry() { - # see https://kind.sigs.k8s.io/docs/user/local-registry/ - echo "${blue}Creating Registry${reset}" - if [ "$CONTAINER_ENGINE" == "docker" ]; then - $CONTAINER_ENGINE run -d --restart=always -p "127.0.0.1:50000:5000" --name "func-registry" registry:2 - $CONTAINER_ENGINE network connect "kind" "func-registry" - elif [ "$CONTAINER_ENGINE" == "podman" ]; then - $CONTAINER_ENGINE run -d --restart=always -p "127.0.0.1:50000:5000" --net=kind --name "func-registry" registry:2 - fi + + # Deploy the registry as a Deployment inside the cluster + $KUBECTL apply -f - < /dev/null; then if [[ "$(uname)" == "Darwin" ]]; then echo "If Docker Desktop was installed via Nix on macOS, you may need to manually configure the insecure registry." - echo "Please confirm \"localhost:50000\" is specified as an insecure registry in the docker config file." + echo "Please confirm \"registry.localtest.me\" is specified as an insecure registry in the docker config file." else echo "If Docker was configured using nix, this command will fail to find daemon.json. please configure the insecure registry by modifying your nix config:" echo " virtualisation.docker = {" echo " enable = true;" - echo " daemon.settings.insecure-registries = [ \"localhost:50000\" ];" + echo " daemon.settings.insecure-registries = [ \"registry.localtest.me\" ];" echo " };" fi fi @@ -68,7 +68,7 @@ warn_nix() { if command -v podman &> /dev/null; then echo "If podman was configured via Nix, this command will likely fail. At time of this writing, podman configured via the nix option 'virtualisation.podman' does not have an option for configuring insecure registries." echo "The configuration required is adding the following to registries.conf:" - echo -e " [[registry-insecure-local]]\n location = \"localhost:50000\"\n insecure = true" + echo -e " [[registry-insecure-local]]\n location = \"registry.localtest.me\"\n insecure = true" fi fi } @@ -91,7 +91,7 @@ set_registry_insecure() { fi # Update daemon.json with insecure registry - patch=".\"insecure-registries\" = [\"localhost:50000\"]" + patch=".\"insecure-registries\" = [\"registry.localtest.me\"]" $USE_SUDO jq "$patch" "$DAEMON_JSON" > /tmp/daemon.json.tmp && $USE_SUDO mv /tmp/daemon.json.tmp "$DAEMON_JSON" echo "OK $DAEMON_JSON" @@ -137,9 +137,9 @@ set_registry_insecure_podman() { echo "# Generated by func registry.sh" >> "$CONFIG_FILE" echo "" >> "$CONFIG_FILE" echo "[[registry]]" >> "$CONFIG_FILE" - echo "location = \"localhost:50000\"" >> "$CONFIG_FILE" + echo "location = \"registry.localtest.me\"" >> "$CONFIG_FILE" echo "insecure = true" >> "$CONFIG_FILE" - echo "Successfully created Podman registry configuration for localhost:50000" + echo "Successfully created Podman registry configuration for registry.localtest.me" return 0 else echo "Could not create user config directory. Skipping Podman registry configuration." @@ -160,61 +160,56 @@ set_registry_insecure_podman() { fi fi - # Check if localhost:50000 is already configured + # Check if registry.localtest.me is already configured ALREADY_CONFIGURED=false if [ "$NEED_SUDO" = true ]; then - if sudo grep -q "localhost:50000" "$CONFIG_FILE" 2>/dev/null; then + if sudo grep -q "registry.localtest.me" "$CONFIG_FILE" 2>/dev/null; then ALREADY_CONFIGURED=true fi else - if grep -q "localhost:50000" "$CONFIG_FILE" 2>/dev/null; then + if grep -q "registry.localtest.me" "$CONFIG_FILE" 2>/dev/null; then ALREADY_CONFIGURED=true fi fi if [ "$ALREADY_CONFIGURED" = true ]; then - echo "localhost:50000 is already configured in $CONFIG_FILE" + echo "registry.localtest.me is already configured in $CONFIG_FILE" return 0 fi # Add the configuration in the appropriate format if [ "$IS_V2_FORMAT" = true ]; then # Use v2 format - echo "Adding localhost:50000 as insecure registry (v2 format)" + echo "Adding registry.localtest.me as insecure registry (v2 format)" if [ "$NEED_SUDO" = true ]; then - echo -e "\n[[registry]]\nlocation = \"localhost:50000\"\ninsecure = true" | sudo tee -a "$CONFIG_FILE" > /dev/null + echo -e "\n[[registry]]\nlocation = \"registry.localtest.me\"\ninsecure = true" | sudo tee -a "$CONFIG_FILE" > /dev/null else - echo -e "\n[[registry]]\nlocation = \"localhost:50000\"\ninsecure = true" >> "$CONFIG_FILE" + echo -e "\n[[registry]]\nlocation = \"registry.localtest.me\"\ninsecure = true" >> "$CONFIG_FILE" fi else # Use v1 format - echo "Adding localhost:50000 as insecure registry (v1 format)" + echo "Adding registry.localtest.me as insecure registry (v1 format)" if [ "$NEED_SUDO" = true ]; then # Check if [registries.insecure] section exists if sudo grep -q "^\[registries.insecure\]" "$CONFIG_FILE" 2>/dev/null; then # Section exists, add to the registries list - sudo sed -i '/^\[registries.insecure\]/a registries = ["localhost:50000"]' "$CONFIG_FILE" + sudo sed -i '/^\[registries.insecure\]/a registries = ["registry.localtest.me"]' "$CONFIG_FILE" else # Section doesn't exist, add it - echo -e "\n[registries.insecure]\nregistries = [\"localhost:50000\"]" | sudo tee -a "$CONFIG_FILE" > /dev/null + echo -e "\n[registries.insecure]\nregistries = [\"registry.localtest.me\"]" | sudo tee -a "$CONFIG_FILE" > /dev/null fi else # Check if [registries.insecure] section exists if grep -q "^\[registries.insecure\]" "$CONFIG_FILE" 2>/dev/null; then # Section exists, add to the registries list - sed -i '/^\[registries.insecure\]/a registries = ["localhost:50000"]' "$CONFIG_FILE" + sed -i '/^\[registries.insecure\]/a registries = ["registry.localtest.me"]' "$CONFIG_FILE" else # Section doesn't exist, add it - echo -e "\n[registries.insecure]\nregistries = [\"localhost:50000\"]" >> "$CONFIG_FILE" + echo -e "\n[registries.insecure]\nregistries = [\"registry.localtest.me\"]" >> "$CONFIG_FILE" fi fi fi - # On macOS, set up SSH port forwarding so Podman VM can access host's localhost:50000 - if [[ "$(uname)" == "Darwin" ]]; then - echo "Setting up port forwarding for Podman VM to access registry..." - podman machine ssh -- -L 50000:localhost:50000 -N -f - fi } if [ "$0" = "${BASH_SOURCE[0]}" ]; then diff --git a/hack/test-integration-podman.sh b/hack/test-integration-podman.sh index 8205312624..9670a5fe8d 100755 --- a/hack/test-integration-podman.sh +++ b/hack/test-integration-podman.sh @@ -5,7 +5,7 @@ unqualified-search-registries = ["docker.io", "quay.io", "registry.fedoraproject short-name-mode="permissive" [[registry]] -location="localhost:50000" +location="registry.localtest.me" insecure=true EOF diff --git a/pkg/builders/builders_int_test.go b/pkg/builders/builders_int_test.go index bef2a1216d..a57fbd58c0 100644 --- a/pkg/builders/builders_int_test.go +++ b/pkg/builders/builders_int_test.go @@ -177,7 +177,7 @@ password=nbusr123 for _, tt := range testCases { var f = f t.Run(tt.Name, func(t *testing.T) { - f.Build.Image = "localhost:50000/go-app:test-" + tt.Name + f.Build.Image = "registry.localtest.me/go-app:test-" + tt.Name f.Build.Builder = tt.Name f.Build.BuilderImages = map[string]string{ tt.Name: tt.BuilderImage(ctx, t, certDir), @@ -274,7 +274,7 @@ func randSN() *big.Int { // Builds a s2i Golang builder that trusts to our self-signed certificate (see createCertificate). func buildPatchedS2IBuilder(ctx context.Context, t *testing.T, certDir string) string { - tag := "localhost:50000/go-toolset:test" + tag := "registry.localtest.me/go-toolset:test" dockerfile := `FROM registry.access.redhat.com/ubi8/go-toolset:latest COPY 85c05568.0 /etc/pki/ca-trust/source/anchors/ USER 0:0 @@ -286,7 +286,7 @@ USER 1001:0 // Builds a tiny paketo builder that trusts to our self-signed certificate (see createCertificate). func buildPatchedBuildpackBuilder(ctx context.Context, t *testing.T, certDir string) string { - tag := "localhost:50000/builder-jammy-tin:test" + tag := "registry.localtest.me/builder-jammy-tin:test" dockerfile := `FROM ghcr.io/knative/builder-jammy-tiny:v2 COPY 85c05568.0 /etc/ssl/certs/ ` diff --git a/pkg/builders/middleware_labels_int_test.go b/pkg/builders/middleware_labels_int_test.go index ff8adc100f..7e79ce58f0 100644 --- a/pkg/builders/middleware_labels_int_test.go +++ b/pkg/builders/middleware_labels_int_test.go @@ -71,13 +71,13 @@ func initFunction(t *testing.T, name string) fn.Function { Name: name, Root: t.TempDir(), Runtime: "go", - Registry: "localhost:50000", + Registry: "registry.localtest.me", } f, err := fn.New().Init(f) if err != nil { t.Fatal(err) } - f.Build.Image = "localhost:50000/" + name + ":latest" + f.Build.Image = "registry.localtest.me/" + name + ":latest" return f } diff --git a/pkg/creds/credentials.go b/pkg/creds/credentials.go index 9176e9676d..9b08aa9036 100644 --- a/pkg/creds/credentials.go +++ b/pkg/creds/credentials.go @@ -56,10 +56,15 @@ func (k keyChain) Resolve(resource authn.Resource) (authn.Authenticator, error) }, nil } -// CheckAuth verifies that credentials can be used for image push -func CheckAuth(ctx context.Context, image string, credentials oci.Credentials, trans http.RoundTripper) error { +// CheckAuth verifies that credentials can be used for image push. +// When insecure is true, the registry is accessed over plain HTTP instead of HTTPS. +func CheckAuth(ctx context.Context, image string, credentials oci.Credentials, trans http.RoundTripper, insecure bool) error { - ref, err := name.ParseReference(image) + var nameOpts []name.Option + if insecure { + nameOpts = append(nameOpts, name.Insecure) + } + ref, err := name.ParseReference(image, nameOpts...) if err != nil { return fmt.Errorf("cannot parse image reference: %w", err) } @@ -90,6 +95,7 @@ type credentialsProvider struct { credentialLoaders []CredentialsCallback authFilePath string transport http.RoundTripper + insecure bool } type Opt func(opts *credentialsProvider) @@ -133,6 +139,14 @@ func WithTransport(transport http.RoundTripper) Opt { } } +// WithInsecure configures the credentials provider to access the registry +// over plain HTTP instead of HTTPS. +func WithInsecure(insecure bool) Opt { + return func(opts *credentialsProvider) { + opts.insecure = insecure + } +} + // WithAdditionalCredentialLoaders adds custom callbacks for credential retrieval. // The callbacks shall return ErrCredentialsNotFound if the credentials are not found. // The callbacks are supposed to be non-interactive as opposed to WithPromptForCredentials. @@ -171,7 +185,7 @@ func NewCredentialsProvider(configPath string, opts ...Opt) oci.CredentialsProvi if c.verifyCredentials == nil { c.verifyCredentials = func(ctx context.Context, registry string, credentials oci.Credentials) error { - return CheckAuth(ctx, registry, credentials, c.transport) + return CheckAuth(ctx, registry, credentials, c.transport, c.insecure) } } diff --git a/pkg/creds/credentials_test.go b/pkg/creds/credentials_test.go index 434471e47a..d2dc54b950 100644 --- a/pkg/creds/credentials_test.go +++ b/pkg/creds/credentials_test.go @@ -213,7 +213,7 @@ func TestCheckAuth(t *testing.T) { } return dialer.DialContext(ctx, network, net.JoinHostPort(h, p)) } - if err := creds.CheckAuth(tt.args.ctx, tt.args.registry+"/someorg/someimage:sometag", c, transport); (err != nil) != tt.wantErr { + if err := creds.CheckAuth(tt.args.ctx, tt.args.registry+"/someorg/someimage:sometag", c, transport, false); (err != nil) != tt.wantErr { t.Errorf("CheckAuth() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -223,7 +223,7 @@ func TestCheckAuth(t *testing.T) { func TestCheckAuthEmptyCreds(t *testing.T) { localhost, _, _ := startServer(t, "", "") - err := creds.CheckAuth(t.Context(), localhost+"/someorg/someimage:sometag", oci.Credentials{}, http.DefaultTransport) + err := creds.CheckAuth(t.Context(), localhost+"/someorg/someimage:sometag", oci.Credentials{}, http.DefaultTransport, false) if err != nil { t.Error(err) } diff --git a/pkg/docker/pusher.go b/pkg/docker/pusher.go index eab81bf01d..4a7ef095a7 100644 --- a/pkg/docker/pusher.go +++ b/pkg/docker/pusher.go @@ -44,6 +44,7 @@ type PusherDockerClientFactory func() (PusherDockerClient, error) // Pusher of images from local to remote registry. type Pusher struct { verbose bool // verbose logging. + insecure bool // use HTTP instead of HTTPS for the registry. credentialsProvider oci.CredentialsProvider transport http.RoundTripper dockerClientFactory PusherDockerClientFactory @@ -73,6 +74,12 @@ func WithVerbose(verbose bool) Opt { } } +func WithInsecure(insecure bool) Opt { + return func(pusher *Pusher) { + pusher.insecure = insecure + } +} + // NewPusher creates an instance of a docker-based image pusher. func NewPusher(opts ...Opt) *Pusher { result := &Pusher{ @@ -90,6 +97,13 @@ func NewPusher(opts ...Opt) *Pusher { return result } +func (n *Pusher) nameOptions() []name.Option { + if n.insecure { + return []name.Option{name.Insecure} + } + return nil +} + func GetRegistry(img string) (string, error) { ref, err := name.ParseReference(img, name.WeakValidation) if err != nil { @@ -121,7 +135,7 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (string, error) { remote.WithTransport(n.transport), } - imgRef, err := name.ParseReference(f.ImageNameWithDigest(imgDigest)) + imgRef, err := name.ParseReference(f.ImageNameWithDigest(imgDigest), n.nameOptions()...) if err != nil { return "", fmt.Errorf("cannot parse image ref: %w", err) } @@ -147,7 +161,7 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (string, error) { Descriptor: *newDesc, }) - idxRef, err := name.ParseReference(f.Build.Image) + idxRef, err := name.ParseReference(f.Build.Image, n.nameOptions()...) if err != nil { return "", fmt.Errorf("cannot parse image index ref: %w", err) } @@ -265,7 +279,7 @@ func (n *Pusher) push(ctx context.Context, f fn.Function, credentials oci.Creden Password: credentials.Password, } - ref, err := name.ParseReference(f.Build.Image) + ref, err := name.ParseReference(f.Build.Image, n.nameOptions()...) if err != nil { return "", err } diff --git a/pkg/functions/client_int_test.go b/pkg/functions/client_int_test.go index 7690f6943a..b474e57621 100644 --- a/pkg/functions/client_int_test.go +++ b/pkg/functions/client_int_test.go @@ -668,6 +668,7 @@ func resetEnv() { func newClient(verbose bool) *fn.Client { return fn.New( fn.WithRegistry(DefaultIntTestRegistry), + fn.WithRegistryInsecure(true), fn.WithScaffolder(oci.NewScaffolder(true)), fn.WithBuilder(oci.NewBuilder("", verbose)), fn.WithPusher(oci.NewPusher(true, true, verbose)), @@ -683,10 +684,11 @@ func newClient(verbose bool) *fn.Client { func newClientWithS2i(verbose bool) *fn.Client { return fn.New( fn.WithRegistry(DefaultIntTestRegistry), + fn.WithRegistryInsecure(true), fn.WithVerbose(verbose), fn.WithScaffolder(s2i.NewScaffolder(true)), fn.WithBuilder(s2i.NewBuilder(s2i.WithVerbose(verbose))), - fn.WithPusher(docker.NewPusher(docker.WithVerbose(verbose))), + fn.WithPusher(docker.NewPusher(docker.WithVerbose(verbose), docker.WithInsecure(true))), fn.WithDeployer(knative.NewDeployer(knative.WithDeployerVerbose(verbose))), fn.WithDescribers(knative.NewDescriber(verbose), k8s.NewDescriber(verbose)), fn.WithRemovers(knative.NewRemover(verbose), k8s.NewRemover(verbose)), diff --git a/pkg/functions/testdata/cluster.yaml b/pkg/functions/testdata/cluster.yaml index 6faa816b5d..ca62d2ba33 100644 --- a/pkg/functions/testdata/cluster.yaml +++ b/pkg/functions/testdata/cluster.yaml @@ -11,7 +11,3 @@ nodes: - containerPort: 30443 hostPort: 443 listenAddress: "127.0.0.1" -containerdConfigPatches: -- |- - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:50000"] - endpoint = ["http://kind-registry:50000"] diff --git a/pkg/knative/deployer.go b/pkg/knative/deployer.go index 1d1f8fd3bb..1bd011d142 100644 --- a/pkg/knative/deployer.go +++ b/pkg/knative/deployer.go @@ -169,11 +169,11 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu daprInstalled = true } - t := fnhttp.NewRoundTripper(fnhttp.WithOpenShiftServiceCA()) + t := fnhttp.NewRoundTripper(fnhttp.WithOpenShiftServiceCA(), fnhttp.WithInsecureSkipVerify(f.RegistryInsecure)) defer func(t fnhttp.RoundTripCloser) { _ = t.Close() }(t) - if err = checkPullPermissions(ctx, k8sClient.CoreV1(), t, f.Deploy.Image, namespace, f.Deploy.ImagePullSecret); err != nil { + if err = checkPullPermissions(ctx, k8sClient.CoreV1(), t, f.Deploy.Image, namespace, f.Deploy.ImagePullSecret, f.RegistryInsecure); err != nil { msg := fmt.Sprintf("warning: error while checking pull secrets: %v", err) switch { case stdErrors.Is(err, errPullSecretNotFound): @@ -664,8 +664,12 @@ func UsesKnativeDeployer(annotations map[string]string) bool { return !ok || deployer == KnativeDeployerName } -func checkPullPermissions(ctx context.Context, core v1.CoreV1Interface, trans http.RoundTripper, img, ns, imagePullSecret string) error { - ref, err := name.ParseReference(img) +func checkPullPermissions(ctx context.Context, core v1.CoreV1Interface, trans http.RoundTripper, img, ns, imagePullSecret string, insecure bool) error { + var nameOpts []name.Option + if insecure { + nameOpts = append(nameOpts, name.Insecure) + } + ref, err := name.ParseReference(img, nameOpts...) if err != nil { return fmt.Errorf("failed to parse image %q: %w", img, err) } diff --git a/pkg/knative/deployer_test.go b/pkg/knative/deployer_test.go index f8989fe4d7..e024ffec6e 100644 --- a/pkg/knative/deployer_test.go +++ b/pkg/knative/deployer_test.go @@ -153,7 +153,7 @@ func TestCheckPullPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err = checkPullPermissions(t.Context(), tt.core, trans, tt.image, "default", "") + err = checkPullPermissions(t.Context(), tt.core, trans, tt.image, "default", "", false) p := tt.errPred if p == nil { p = func(err error) bool { @@ -184,7 +184,7 @@ func TestCheckPullPermissions_FunctionImagePullSecret(t *testing.T) { } coreClient := fake.NewClientset(sa, secretA).CoreV1() - err := checkPullPermissions(t.Context(), coreClient, trans, securedImage, "default", secretA.Name) + err := checkPullPermissions(t.Context(), coreClient, trans, securedImage, "default", secretA.Name, false) if err != nil { t.Errorf("expected no error with valid function-level pull secret, got: %v", err) } @@ -197,7 +197,7 @@ func TestCheckPullPermissions_FunctionImagePullSecret(t *testing.T) { } coreClient := fake.NewClientset(sa, badSecret).CoreV1() - err := checkPullPermissions(t.Context(), coreClient, trans, securedImage, "default", badSecret.Name) + err := checkPullPermissions(t.Context(), coreClient, trans, securedImage, "default", badSecret.Name, false) if !errors.Is(err, errOnlyIncorrectPullSecretFound) { t.Errorf("expected errOnlyIncorrectPullSecretFound, got: %v", err) } @@ -205,7 +205,7 @@ func TestCheckPullPermissions_FunctionImagePullSecret(t *testing.T) { t.Run("no function-level secret falls back to SA", func(t *testing.T) { coreClient := core(secretA) - err := checkPullPermissions(t.Context(), coreClient, trans, securedImage, "default", "") + err := checkPullPermissions(t.Context(), coreClient, trans, securedImage, "default", "", false) if err != nil { t.Errorf("expected no error when SA has valid pull secret, got: %v", err) } diff --git a/pkg/mcp/instructions.md b/pkg/mcp/instructions.md index da991fa66a..b16f30b35d 100644 --- a/pkg/mcp/instructions.md +++ b/pkg/mcp/instructions.md @@ -87,20 +87,20 @@ The help text provides authoritative parameter information and usage context. A common challenge with users is determining the right value for "registry". This is composed of two parts: -1. **Registry domain:** docker.io, ghcr.io, localhost:50000 +1. **Registry domain:** docker.io, ghcr.io, registry.localtest.me 2. **Registry user:** alice, func, etc. When combined this constitutes a full "registry" location for the Function's built image. **Examples:** - `docker.io/alice` -- `localhost:50000/func` +- `registry.localtest.me/func` The final Function image will then have the Function name as a suffix along with the :latest tag (example: `docker.io/alice/myfunc:latest`), but this is hidden from the user unless they want to fully override this behavior and supply their own custom value for the image parameter. **Important guidance:** - It is important to carefully guide the user through the creation of this registry argument, as this is often the most challenging part of getting a Function deployed the first time -- Ask for the registry. If they only provide the DOMAIN part (eg docker.io or localhost:50000), ask them to either confirm there is no registry user part or provide it +- Ask for the registry. If they only provide the DOMAIN part (eg docker.io or registry.localtest.me), ask them to either confirm there is no registry user part or provide it - The final value is the two concatenated with a forward slash - Subsequent deployments automatically reuse the last setting, so this should only be asked for on those first deployments - BE SURE to verify the final format of this value as consisting of both a DOMAIN part and a USER part diff --git a/pkg/testing/testing.go b/pkg/testing/testing.go index 2be51f971c..e006b0265b 100644 --- a/pkg/testing/testing.go +++ b/pkg/testing/testing.go @@ -30,7 +30,7 @@ import ( "testing" ) -const DefaultIntTestRegistry = "localhost:50000/func" +const DefaultIntTestRegistry = "registry.localtest.me/func" // Using the given path, create it as a new directory and return a deferrable // which will remove it.