Skip to content

Commit

Permalink
Fix external registry issues (#1230)
Browse files Browse the repository at this point in the history
## Description

This refactors a lot of how we interact with registries (particularly
external ones) to address the following bugs:

- The inability to use registries external to the current cluster
- The inability to set the NodePort of the internal Zarf registry

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [X] Test, docs, adr added or updated as needed
- [X] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow)
followed
  • Loading branch information
Racer159 authored Jan 30, 2023
1 parent 5925111 commit 1cbc7a3
Show file tree
Hide file tree
Showing 21 changed files with 325 additions and 106 deletions.
8 changes: 2 additions & 6 deletions src/cmd/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package cmd

import (
"net/url"
"os"

"github.com/anchore/syft/cmd/syft/cli"
Expand Down Expand Up @@ -234,16 +233,13 @@ func zarfCraneCatalog(cranePlatformOptions *[]crane.Option) *cobra.Command {
return err
}
tunnelReg.Connect(cluster.ZarfRegistry, false)
registryURL, err := url.Parse(tunnelReg.HTTPEndpoint())
if err != nil {
return err
}

// Add the correct authentication to the crane command options
authOption := config.GetCraneAuthOption(zarfState.RegistryInfo.PullUsername, zarfState.RegistryInfo.PullPassword)
*cranePlatformOptions = append(*cranePlatformOptions, authOption)
registryEndpoint := tunnelReg.Endpoint()

return originalCatalogFn(cmd, []string{registryURL.Host})
return originalCatalogFn(cmd, []string{registryEndpoint})
}

return craneCatalog
Expand Down
11 changes: 0 additions & 11 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const (
ZarfSBOMDir = "zarf-sbom"
ZarfPackagePrefix = "zarf-package-"

ZarfInClusterContainerRegistryURL = "http://zarf-registry-http.zarf.svc.cluster.local:5000"
ZarfInClusterContainerRegistryNodePort = 31999

ZarfInClusterGitServiceURL = "http://zarf-gitea-http.zarf.svc.cluster.local:3000"
Expand Down Expand Up @@ -165,16 +164,6 @@ func GetValidPackageExtensions() [3]string {
return [...]string{".tar.zst", ".tar", ".zip"}
}

// GetRegistry returns the registry URL based on the Zarf state.
func GetRegistry(state types.ZarfState) string {
// If a node port is populated, then we are using a registry internal to the cluster. Ignore the provided address and use localhost
if state.RegistryInfo.NodePort >= 30000 {
return fmt.Sprintf("%s:%d", IPV4Localhost, state.RegistryInfo.NodePort)
}

return state.RegistryInfo.Address
}

// GetAbsCachePath gets the absolute cache path for images and git repos.
func GetAbsCachePath() string {
homePath, _ := os.UserHomeDir()
Expand Down
3 changes: 1 addition & 2 deletions src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,5 @@ const (

// Collection of reusable error messages.
var (
ErrInitNotFound = errors.New("this command requires a zarf-init package, but one was not found on the local system. Re-run the last command again without '--confirm' to download the package")
ErrNotAServiceURL = errors.New("the provided URL does not match service url format of http://{SERVICE_NAME}.{NAMESPACE}.svc.cluster.local:{PORT}")
ErrInitNotFound = errors.New("this command requires a zarf-init package, but one was not found on the local system. Re-run the last command again without '--confirm' to download the package")
)
2 changes: 1 addition & 1 deletion src/internal/agent/hooks/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) {
if err != nil {
return nil, fmt.Errorf(lang.AgentErrGetState, err)
}
containerRegistryURL := config.GetRegistry(zarfState)
containerRegistryURL := zarfState.RegistryInfo.Address

// update the image host for each init container
for idx, container := range pod.Spec.InitContainers {
Expand Down
2 changes: 1 addition & 1 deletion src/internal/agent/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func StartWebhook() {
}
}()

message.Info(lang.AgentErrStart)
message.Infof(lang.AgentInfoPort, httpPort)

// listen shutdown signal
signalChan := make(chan os.Signal, 1)
Expand Down
2 changes: 1 addition & 1 deletion src/internal/cluster/injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (c *Cluster) injectorIsReady(spinner *message.Spinner) bool {

spinner.Updatef("Testing the injector for seed image availability")

seedRegistry := fmt.Sprintf("http://%s/v2/library/%s/manifests/%s", tunnel.Endpoint(), config.ZarfSeedImage, config.ZarfSeedTag)
seedRegistry := fmt.Sprintf("%s/v2/library/%s/manifests/%s", tunnel.HTTPEndpoint(), config.ZarfSeedImage, config.ZarfSeedTag)
if resp, err := http.Get(seedRegistry); err != nil || resp.StatusCode != 200 {
// Just debug log the output because failures just result in trying the next image
message.Debug(resp, err)
Expand Down
3 changes: 1 addition & 2 deletions src/internal/cluster/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

corev1 "k8s.io/api/core/v1"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/pkg/message"
)

Expand Down Expand Up @@ -48,7 +47,7 @@ func (c *Cluster) GenerateRegistryPullCreds(namespace, name string) (*corev1.Sec
fieldValue := zarfState.RegistryInfo.PullUsername + ":" + credential
authEncodedValue := base64.StdEncoding.EncodeToString([]byte(fieldValue))

registry := config.GetRegistry(zarfState)
registry := zarfState.RegistryInfo.Address
// Create the expected structure for the dockerconfigjson
dockerConfigJSON := DockerConfig{
Auths: DockerConfigEntry{
Expand Down
8 changes: 6 additions & 2 deletions src/internal/cluster/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,15 @@ func (c *Cluster) PostSeedRegistry(tempPath types.TempPaths) error {
}

func (c *Cluster) fillInEmptyContainerRegistryValues(containerRegistry types.RegistryInfo) types.RegistryInfo {
// Set default NodePort if none was provided
if containerRegistry.NodePort == 0 {
containerRegistry.NodePort = config.ZarfInClusterContainerRegistryNodePort
}

// Set default url if an external registry was not provided
if containerRegistry.Address == "" {
containerRegistry.InternalRegistry = true
containerRegistry.NodePort = config.ZarfInClusterContainerRegistryNodePort
containerRegistry.Address = fmt.Sprintf("http://%s:%d", config.IPV4Localhost, containerRegistry.NodePort)
containerRegistry.Address = fmt.Sprintf("%s:%d", config.IPV4Localhost, containerRegistry.NodePort)
}

// Generate a push-user password if not provided by init flag
Expand Down
87 changes: 66 additions & 21 deletions src/internal/cluster/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"syscall"
"time"

"github.com/defenseunicorns/zarf/src/config/lang"
"github.com/defenseunicorns/zarf/src/types"

"github.com/defenseunicorns/zarf/src/config"
Expand Down Expand Up @@ -67,6 +66,13 @@ type Tunnel struct {
spinner *message.Spinner
}

// ServiceInfo contains information necessary for connecting to a cluster service.
type ServiceInfo struct {
Namespace string
Name string
Port int
}

// PrintConnectTable will print a table of all Zarf connect matches found in the cluster.
func (c *Cluster) PrintConnectTable() error {
list, err := c.Kube.GetServicesByLabelExists(v1.NamespaceAll, config.ZarfConnectLabelName)
Expand All @@ -91,33 +97,72 @@ func (c *Cluster) PrintConnectTable() error {
return nil
}

// IsServiceURL will check if the provided string is a valid serviceURL based on if it properly matches a validating regexp.
func IsServiceURL(serviceURL string) bool {
parsedURL, err := url.Parse(serviceURL)
// ServiceInfoFromNodePortURL takes a nodePortURL and parses it to find the service info for connecting to the cluster. The string is expected to follow the following format:
// Example nodePortURL: 127.0.0.1:{PORT}.
func ServiceInfoFromNodePortURL(nodePortURL string) *ServiceInfo {
// Attempt to parse as normal, if this fails add a scheme to the URL (docker registries don't use schemes)
parsedURL, err := url.Parse(nodePortURL)
if err != nil {
return false
parsedURL, err = url.Parse("scheme://" + nodePortURL)
if err != nil {
return nil
}
}

// Match hostname against local cluster service format
pattern := regexp.MustCompile(serviceURLPattern)
matches := pattern.FindStringSubmatch(parsedURL.Hostname())
// Match hostname against localhost ip/hostnames
hostname := parsedURL.Hostname()
if hostname != config.IPV4Localhost && hostname != "localhost" {
return nil
}

// Get the node port from the nodeportURL.
nodePort, err := strconv.Atoi(parsedURL.Port())
if err != nil {
return nil
}
if nodePort < 30000 || nodePort > 32767 {
return nil
}

kube, err := k8s.NewWithWait(message.Debugf, labels, defaultTimeout)
if err != nil {
return nil
}

// If incomplete match, return an error
return len(matches) == 3
services, err := kube.GetServices("")
if err != nil {
return nil
}

for _, svc := range services.Items {
if svc.Spec.Type == "NodePort" {
for _, port := range svc.Spec.Ports {
if int(port.NodePort) == nodePort {
return &ServiceInfo{
Namespace: svc.Namespace,
Name: svc.Name,
Port: int(port.Port),
}
}
}
}
}

return nil
}

// NewTunnelFromServiceURL takes a serviceURL and parses it to create a tunnel to the cluster. The string is expected to follow the following format:
// ServiceInfoFromServiceURL takes a serviceURL and parses it to find the service info for connecting to the cluster. The string is expected to follow the following format:
// Example serviceURL: http://{SERVICE_NAME}.{NAMESPACE}.svc.cluster.local:{PORT}.
func NewTunnelFromServiceURL(serviceURL string) (*Tunnel, error) {
func ServiceInfoFromServiceURL(serviceURL string) *ServiceInfo {
parsedURL, err := url.Parse(serviceURL)
if err != nil {
return nil, err
return nil
}

// Get the remote port from the serviceURL.
remotePort, err := strconv.Atoi(parsedURL.Port())
if err != nil {
return nil, err
return nil
}

// Match hostname against local cluster service format.
Expand All @@ -126,14 +171,14 @@ func NewTunnelFromServiceURL(serviceURL string) (*Tunnel, error) {

// If incomplete match, return an error.
if len(matches) != 3 {
return nil, lang.ErrNotAServiceURL
return nil
}

// Use the matched values to create a new tunnel.
name := matches[pattern.SubexpIndex("name")]
namespace := matches[pattern.SubexpIndex("namespace")]

return NewTunnel(namespace, SvcResource, name, 0, remotePort)
return &ServiceInfo{
Namespace: matches[pattern.SubexpIndex("namespace")],
Name: matches[pattern.SubexpIndex("name")],
Port: remotePort,
}
}

// NewTunnel will create a new Tunnel struct.
Expand Down Expand Up @@ -261,7 +306,7 @@ func (tunnel *Tunnel) Connect(target string, blocking bool) error {
return nil
}

// Endpoint returns the tunnel endpoint.
// Endpoint returns the tunnel ip address and port (i.e. for docker registries)
func (tunnel *Tunnel) Endpoint() string {
message.Debug("tunnel.Endpoint()")
return fmt.Sprintf("127.0.0.1:%d", tunnel.localPort)
Expand Down
10 changes: 5 additions & 5 deletions src/internal/packager/git/gitea.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ func (g *Git) CreateReadOnlyUser() error {
tunnel.Connect(cluster.ZarfGit, false)
defer tunnel.Close()

tunnelURL := tunnel.Endpoint()
tunnelURL := tunnel.HTTPEndpoint()

// Determine if the read only user already exists
getUserEndpoint := fmt.Sprintf("http://%s/api/v1/admin/users", tunnelURL)
getUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users", tunnelURL)
getUserRequest, _ := netHttp.NewRequest("GET", getUserEndpoint, nil)
out, err := g.DoHTTPThings(getUserRequest, g.Server.PushUsername, g.Server.PushPassword)
message.Debugf("GET %s:\n%s", getUserEndpoint, string(out))
Expand Down Expand Up @@ -60,7 +60,7 @@ func (g *Git) CreateReadOnlyUser() error {
"password": g.Server.PullPassword,
}
updateUserData, _ := json.Marshal(updateUserBody)
updateUserEndpoint := fmt.Sprintf("http://%s/api/v1/admin/users/%s", tunnelURL, g.Server.PullUsername)
updateUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users/%s", tunnelURL, g.Server.PullUsername)
updateUserRequest, _ := netHttp.NewRequest("PATCH", updateUserEndpoint, bytes.NewBuffer(updateUserData))
out, err = g.DoHTTPThings(updateUserRequest, g.Server.PushUsername, g.Server.PushPassword)
message.Debugf("PATCH %s:\n%s", updateUserEndpoint, string(out))
Expand All @@ -80,7 +80,7 @@ func (g *Git) CreateReadOnlyUser() error {
}

// Send API request to create the user
createUserEndpoint := fmt.Sprintf("http://%s/api/v1/admin/users", tunnelURL)
createUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users", tunnelURL)
createUserRequest, _ := netHttp.NewRequest("POST", createUserEndpoint, bytes.NewBuffer(createUserData))
out, err = g.DoHTTPThings(createUserRequest, g.Server.PushUsername, g.Server.PushPassword)
message.Debugf("POST %s:\n%s", createUserEndpoint, string(out))
Expand All @@ -95,7 +95,7 @@ func (g *Git) CreateReadOnlyUser() error {
"allow_create_organization": false,
}
updateUserData, _ := json.Marshal(updateUserBody)
updateUserEndpoint := fmt.Sprintf("http://%s/api/v1/admin/users/%s", tunnelURL, g.Server.PullUsername)
updateUserEndpoint := fmt.Sprintf("%s/api/v1/admin/users/%s", tunnelURL, g.Server.PullUsername)
updateUserRequest, _ := netHttp.NewRequest("PATCH", updateUserEndpoint, bytes.NewBuffer(updateUserData))
out, err = g.DoHTTPThings(updateUserRequest, g.Server.PushUsername, g.Server.PushPassword)
message.Debugf("PATCH %s:\n%s", updateUserEndpoint, string(out))
Expand Down
19 changes: 12 additions & 7 deletions src/internal/packager/images/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,28 @@ func (i *ImgConfig) PushToZarfRegistry() error {
return err
}
target = cluster.ZarfRegistry
} else if cluster.IsServiceURL(i.RegInfo.Address) {
// If this is a serviceURL, create a port-forward tunnel to that resource
if tunnel, err = cluster.NewTunnelFromServiceURL(i.RegInfo.Address); err != nil {
return err
} else {
svcInfo := cluster.ServiceInfoFromNodePortURL(i.RegInfo.Address)
if svcInfo != nil {
// If this is a service, create a port-forward tunnel to that resource
if tunnel, err = cluster.NewTunnel(svcInfo.Namespace, cluster.SvcResource, svcInfo.Name, 0, svcInfo.Port); err != nil {
return err
}
}
}

if tunnel != nil {
tunnel.Connect(target, false)
defer tunnel.Close()
registryURL = tunnel.Endpoint()
} else {
registryURL = i.RegInfo.Address
}

spinner := message.NewProgressSpinner("Storing images in the zarf registry")
defer spinner.Stop()

pushOptions := config.GetCraneAuthOption(i.RegInfo.PushUsername, i.RegInfo.PushPassword)
pushOptions := []crane.Option{config.GetCraneAuthOption(i.RegInfo.PushUsername, i.RegInfo.PushPassword)}
message.Debugf("crane pushOptions = %#v", pushOptions)

for _, src := range i.ImgList {
Expand All @@ -65,7 +70,7 @@ func (i *ImgConfig) PushToZarfRegistry() error {

message.Debugf("crane.Push() %s:%s -> %s)", i.TarballPath, src, offlineNameCRC)

if err = crane.Push(img, offlineNameCRC, pushOptions); err != nil {
if err = crane.Push(img, offlineNameCRC, pushOptions...); err != nil {
return err
}
}
Expand All @@ -79,7 +84,7 @@ func (i *ImgConfig) PushToZarfRegistry() error {

message.Debugf("crane.Push() %s:%s -> %s)", i.TarballPath, src, offlineName)

if err = crane.Push(img, offlineName, pushOptions); err != nil {
if err = crane.Push(img, offlineName, pushOptions...); err != nil {
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/internal/packager/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func Generate(cfg *types.PackagerConfig) (Values, error) {

generated.htpasswd = fmt.Sprintf("%s\\n%s", pushUser, pullUser)

generated.registry = config.GetRegistry(cfg.State)
generated.registry = regInfo.Address

return generated, nil
}
Expand Down
5 changes: 5 additions & 0 deletions src/pkg/k8s/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ func (k *K8s) GetService(namespace, serviceName string) (*corev1.Service, error)
return k.Clientset.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
}

// GetServices returns a list of services in the provided namespace. To search all namespaces, pass "" in the namespace arg.
func (k *K8s) GetServices(namespace string) (*corev1.ServiceList, error) {
return k.Clientset.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{})
}

// GetServicesByLabel returns a list of matched services given a label and value. To search all namespaces, pass "" in the namespace arg.
func (k *K8s) GetServicesByLabel(namespace, label, value string) (*corev1.ServiceList, error) {
// Creat the selector and add the requirement
Expand Down
9 changes: 5 additions & 4 deletions src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,17 +401,18 @@ func (p *Packager) pushReposToRepository(reposPath string, repos []string) error
tryPush := func() error {
gitClient := git.New(p.cfg.State.GitServer)

// If this is a serviceURL, create a port-forward tunnel to that resource
if cluster.IsServiceURL(gitClient.Server.Address) {
tunnel, err := cluster.NewTunnelFromServiceURL(gitClient.Server.Address)
svcInfo := cluster.ServiceInfoFromServiceURL(gitClient.Server.Address)
// If this is a service, create a port-forward tunnel to that resource
if svcInfo != nil {
tunnel, err := cluster.NewTunnel(svcInfo.Namespace, cluster.SvcResource, svcInfo.Name, 0, svcInfo.Port)

if err != nil {
return err
}

tunnel.Connect("", false)
defer tunnel.Close()
gitClient.Server.Address = fmt.Sprintf("http://%s", tunnel.Endpoint())
gitClient.Server.Address = tunnel.HTTPEndpoint()
}

// Convert the repo URL to a Zarf-formatted repo name
Expand Down
Loading

0 comments on commit 1cbc7a3

Please sign in to comment.