Skip to content

Commit

Permalink
Add support for the Authenticated Registries (vmware-tanzu#744)
Browse files Browse the repository at this point in the history
To use registries that require authentication to host Tanzu CLI Plugins images users are expected to do the following:
1. Use `docker login <registry>` or `crane auth login <registry>` to authenticate with the registry
2. Specify environment variable `TANZU_CLI_AUTHENTICATED_REGISTRY=<registry>`. By specifying this environment variable, Tanzu CLI will use the default authentication mechanism instead of using Anonymous access to fetch images.
  • Loading branch information
anujc25 authored and vuil committed May 1, 2024
1 parent 801487d commit 6449943
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Below is the list of these variables:
| -------------------- | ----------- | ----- |
| `SQL_STATEMENTS_LOG_FILE` | Specifies a log file where SQL commands will be logged when _modifying_ the plugin inventory database. This is done when publishing plugins using the `builder` plugin. | A file name with its path |
| `TANZU_CLI_ADDITIONAL_PLUGIN_DISCOVERY_IMAGES_TEST_ONLY` | Specifies test plugin repositories to use as a supplement to the production Central Repository of plugins. Ignored if `TANZU_CLI_PRIVATE_PLUGIN_DISCOVERY_IMAGES` is set. | Comma-separated list of test plugin repository URIs| |
| `TANZU_CLI_AUTHENTICATED_REGISTRY` | Specifies the list of registry hosts that requires authentication to pull images. Tanzu CLI will use default docker auth to communicate to these registries | Comma-separated list of registry host-names | |
| `TANZU_CLI_ESSENTIALS_PLUGIN_GROUP_NAME` | Override the default name (`vmware-tanzucli/essentials`) of the Essential Plugins group. Should not be needed. | Group name |
| `TANZU_CLI_ESSENTIALS_PLUGIN_GROUP_VERSION` | Specify a fixed version to use for the Essential Plugins group instead of the latest. Should not be needed. | Group version |
| `TANZU_CLI_SKIP_CONTEXT_RECOMMENDED_PLUGIN_INSTALLATION` | Skips the auto-installation of the context recommended plugins
Expand Down
12 changes: 10 additions & 2 deletions pkg/carvelhelpers/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
package carvelhelpers

import (
"os"
"strings"

"github.com/pkg/errors"

ctlimg "github.com/vmware-tanzu/carvel-imgpkg/pkg/imgpkg/registry"

"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
"github.com/vmware-tanzu/tanzu-cli/pkg/registry"
"github.com/vmware-tanzu/tanzu-cli/pkg/utils"
)

// GetFilesMapFromImage returns map of files metadata
Expand All @@ -32,8 +37,11 @@ func GetImageDigest(imageWithTag string) (string, string, error) {
// newRegistry returns a new registry object by also taking
// into account for any custom registry provided by the user
func newRegistry(registryHost string) (registry.Registry, error) {
registryOpts := &ctlimg.Opts{
Anon: true,
registryOpts := &ctlimg.Opts{}

authenticatedRegistries := strings.Split(os.Getenv(constants.AuthenticatedRegistry), ",")
if !utils.ContainsRegistry(authenticatedRegistries, registryHost) {
registryOpts.Anon = true
}

regCertOptions, err := registry.GetRegistryCertOptions(registryHost)
Expand Down
4 changes: 4 additions & 0 deletions pkg/constants/env_variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,8 @@ const (
// SkipTAPScopesValidationOnTanzuContext skips the TAP scopes validation on the token acquired while creating "tanzu"
// context using tanzu login or tanzu context create command
SkipTAPScopesValidationOnTanzuContext = "TANZU_CLI_SKIP_TAP_SCOPES_VALIDATION_ON_TANZU_CONTEXT"

// AuthenticatedRegistry provides a comma separated list of registry hosts that requires authentication
// to pull images. Tanzu CLI will use default docker auth to communicate to these registries
AuthenticatedRegistry = "TANZU_CLI_AUTHENTICATED_REGISTRY"
)
19 changes: 19 additions & 0 deletions pkg/utils/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,22 @@ func JoinURL(baseURL, relativeURL string) (string, error) {
// Return the joined URL as a string
return parsedBaseURL.String(), nil
}

// ContainsRegistry returns true if the specified registryHost is part of registries
func ContainsRegistry(registries []string, registryHost string) bool {
cleanRegistryURL := func(u string) string {
u = strings.TrimSpace(u)
u = strings.TrimPrefix(u, "http://")
u = strings.TrimPrefix(u, "https://")
return strings.Split(u, "/")[0]
}
registryHost = cleanRegistryURL(registryHost)

for _, reg := range registries {
reg = cleanRegistryURL(reg)
if strings.EqualFold(reg, registryHost) {
return true
}
}
return false
}
28 changes: 28 additions & 0 deletions pkg/utils/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,3 +630,31 @@ func TestJoinUrl(t *testing.T) {
})
}
}

func TestContainsRegistry(t *testing.T) {
testCases := []struct {
input string
expected bool
}{
{"google.com", true},
{"facebook.com", false},
{"xyz.com", false},
{"packages.xyz.com", true},
{"packages.example.com", false},
{"projects.example.com", true},
{"packages.repo.com", false},
{"registry.packages.repo.com", true},
{"registry.packages.repo.com/projects", true},
{"https://registry.packages.repo.com/projects", true},
{"registry.packages", false},
}

registries := []string{"https://google.com", "http://fb.com", "https://packages.xyz.com", "projects.example.com", "registry.packages.repo.com/projects/cli"}

for _, tc := range testCases {
got := ContainsRegistry(registries, tc.input)
if got != tc.expected {
t.Errorf("ContainsRegistry(%q, %q) = %t; want %t", registries, tc.input, got, tc.expected)
}
}
}
4 changes: 3 additions & 1 deletion test/e2e/airgapped/airgapped_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
e2eAirgappedCentralRepoWithAuth string
e2eAirgappedCentralRepoWithAuthUsername string
e2eAirgappedCentralRepoWithAuthPassword string
e2eAirgappedCentralRepoWithAuthImage string
pluginsSearchList []*framework.PluginInfo
pluginGroups []*framework.PluginGroup
tempDir string
Expand All @@ -58,8 +59,9 @@ var _ = BeforeSuite(func() {
Expect(e2eAirgappedCentralRepoWithAuthUsername).NotTo(BeEmpty(), fmt.Sprintf("environment variable %s should set with airgapped central repository URL", framework.TanzuCliE2ETestAirgappedRepoWithAuthUsername))
e2eAirgappedCentralRepoWithAuthPassword = os.Getenv(framework.TanzuCliE2ETestAirgappedRepoWithAuthPassword)
Expect(e2eAirgappedCentralRepoWithAuthPassword).NotTo(BeEmpty(), fmt.Sprintf("environment variable %s should set with airgapped central repository URL", framework.TanzuCliE2ETestAirgappedRepoWithAuthPassword))
e2eAirgappedCentralRepoWithAuthImage = fmt.Sprintf("%s%s", e2eAirgappedCentralRepoWithAuth, filepath.Base(e2eTestLocalCentralRepoImage))

os.Setenv(framework.TanzuCliPluginDiscoverySignatureVerificationSkipList, fmt.Sprintf("%v,%v", e2eAirgappedCentralRepoImage, e2eTestLocalCentralRepoImage))
os.Setenv(framework.TanzuCliPluginDiscoverySignatureVerificationSkipList, fmt.Sprintf("%v,%v,%v", e2eAirgappedCentralRepoImage, e2eTestLocalCentralRepoImage, e2eAirgappedCentralRepoWithAuthImage))

e2eTestLocalCentralRepoPluginHost := os.Getenv(framework.TanzuCliE2ETestLocalCentralRepositoryHost)
Expect(e2eTestLocalCentralRepoPluginHost).NotTo(BeEmpty(), fmt.Sprintf("environment variable %s should set with local central repository host", framework.TanzuCliE2ETestLocalCentralRepositoryHost))
Expand Down
57 changes: 50 additions & 7 deletions test/e2e/airgapped/airgapped_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,20 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download
})
})
Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests with plugin group 'vmware-tkg/default:v0.0.1'", func() {
originalHomeDir := framework.GetHomeDir()

// Test case: download plugin bundle for plugin-group vmware-tkg/default:v0.0.1
It("download plugin bundle with specific plugin-group vmware-tkg/default:v0.0.1", func() {
err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tkg/default:v0.0.1"}, []string{}, false, filepath.Join(tempDir, "plugin_bundle_vmware-tkg-default-v0.0.1.tar.gz"))
Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle with specific group")
})

// Test case: upload plugin bundle downloaded using vmware-tkg/default:v0.0.1 plugin-group to the airgapped repository with authentication
It("upload plugin bundle that was downloaded using vmware-tkg/default:v0.0.1 plugin-group to the airgapped repository with authentication", func() {
curHomeDir := framework.GetHomeDir()
defer func() {
os.Setenv("HOME", curHomeDir)
}()
It("Authenticated Registry: Docker Login", func() {
// We are resetting the HOME environment variable for this specific tests as when we do docker login we need to have actually HOME variable set correctly
// otherwise the docker login fails with different errors on different systems (on macos we see keychain specific error)
// We are also using above defer function to revert the HOME environment variable after this test is ran
// We are reverting the HOME environment variable after this set of authenticated registry tests are run as part of "Authenticated Registry: Unset Environment Variable" test
os.Setenv("HOME", framework.OriginalHomeDir)
os.Setenv("TANZU_CLI_AUTHENTICATED_REGISTRY", e2eAirgappedCentralRepoWithAuth)

// Try uploading plugin bundle without docker login, it should fail
err := tf.PluginCmd.UploadPluginBundle(e2eAirgappedCentralRepoWithAuth, filepath.Join(tempDir, "plugin_bundle_vmware-tkg-default-v0.0.1.tar.gz"))
Expand All @@ -92,12 +90,57 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download
dockerloginCmd := fmt.Sprintf("docker login %s --username %s --password %s", e2eAirgappedCentralRepoWithAuth, e2eAirgappedCentralRepoWithAuthUsername, e2eAirgappedCentralRepoWithAuthPassword)
_, _, err = tf.Exec.Exec(dockerloginCmd)
Expect(err).To(BeNil())
})

// Test case: upload plugin bundle downloaded using vmware-tkg/default:v0.0.1 plugin-group to the airgapped repository with authentication
It("Authenticated Registry: upload plugin bundle that was downloaded using vmware-tkg/default:v0.0.1 plugin-group to the airgapped repository with authentication", func() {
// Try uploading plugin bundle after docker login, it should succeed
err = tf.PluginCmd.UploadPluginBundle(e2eAirgappedCentralRepoWithAuth, filepath.Join(tempDir, "plugin_bundle_vmware-tkg-default-v0.0.1.tar.gz"))
Expect(err).To(BeNil(), "should not get any error while uploading plugin bundle")
})

// Test case: validate that the updating the discovery source to point to new airgapped repository requiring authentication works
It("Authenticated Registry: update discovery source to point to new airgapped repository discovery image that requires authentication", func() {
err = framework.UpdatePluginDiscoverySource(tf, e2eAirgappedCentralRepoWithAuthImage)
Expect(err).To(BeNil(), "should not get any error for plugin source update for authenticated registry")
})

// Test case: Validate that the correct plugins and plugin group exists with `tanzu plugin search` and `tanzu plugin group search` output on authenticated registry
It("Authenticated Registry: validate the plugins from group 'vmware-tkg/default:v0.0.1' exists", func() {
// search plugin groups
pluginGroups, err = pluginlifecyclee2e.SearchAllPluginGroups(tf)
Expect(err).To(BeNil(), framework.NoErrorForPluginGroupSearch)
// check all expected plugin groups are available in the `plugin group search` output from the airgapped repository
expectedPluginGroups := []*framework.PluginGroup{{Group: "vmware-tkg/default", Latest: "v0.0.1", Description: "Desc for vmware-tkg/default:v0.0.1"}}
Expect(framework.IsAllPluginGroupsExists(pluginGroups, expectedPluginGroups)).Should(BeTrue(), "all required plugin groups for life cycle tests should exists in plugin group search output")

// search plugins and make sure correct number of plugins available
// check expected plugins are available in the `plugin search` output from the airgapped repository
expectedPlugins := pluginsForPGTKG001
expectedPlugins = append(expectedPlugins, essentialPlugins...) // Essential plugin will be always installed
pluginsSearchList, err = pluginlifecyclee2e.SearchAllPlugins(tf)
Expect(err).To(BeNil(), framework.NoErrorForPluginSearch)
Expect(len(pluginsSearchList)).To(Equal(len(expectedPlugins)))
Expect(framework.CheckAllPluginsExists(pluginsSearchList, expectedPlugins)).To(BeTrue())
})

// Test case: Validate that the plugins can be installed from the plugin-group for authenticated registry
It("Authenticated Registry: validate that plugins can be installed from group 'vmware-tkg/default:v0.0.1' for authenticated registry", func() {
// All plugins should get installed from the group
_, _, err := tf.PluginCmd.InstallPluginsFromGroup("", "vmware-tkg/default:v0.0.1")
Expect(err).To(BeNil())

// Verify all plugins got installed with `tanzu plugin list`
installedPlugins, err := tf.PluginCmd.ListInstalledPlugins()
Expect(err).To(BeNil())
Expect(framework.CheckAllPluginsExists(installedPlugins, pluginsForPGTKG001)).To(BeTrue())
})

It("Authenticated Registry: Unset Environment Variable", func() {
os.Setenv("HOME", originalHomeDir)
os.Unsetenv("TANZU_CLI_AUTHENTICATED_REGISTRY")
})

// Test case: upload plugin bundle downloaded using vmware-tkg/default:v0.0.1 plugin-group to the airgapped repository
It("upload plugin bundle that was downloaded using vmware-tkg/default:v0.0.1 plugin-group to the airgapped repository", func() {
err := tf.PluginCmd.UploadPluginBundle(e2eAirgappedCentralRepo, filepath.Join(tempDir, "plugin_bundle_vmware-tkg-default-v0.0.1.tar.gz"))
Expand Down

0 comments on commit 6449943

Please sign in to comment.