Skip to content

Commit

Permalink
#1570. Connect Action (#1638)
Browse files Browse the repository at this point in the history
## Description
Ashton should be able to connect to a pre-defined connection within a
deployed package.



## Related Issue
#1570 


## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] 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

---------

Co-authored-by: Wayne Starr <[email protected]>
  • Loading branch information
mike-winberry and Racer159 authored May 8, 2023
1 parent 2a5adf2 commit 5fcdc3c
Show file tree
Hide file tree
Showing 23 changed files with 968 additions and 4,362 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test-ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ jobs:
export NODE_PATH=$(pwd)/src/ui/node_modules &&
npm --prefix src/ui run test:pre-init &&
npm --prefix src/ui run test:init &&
npm --prefix src/ui run test:post-init
npm --prefix src/ui run test:post-init &&
npm --prefix src/ui run test:connect
- name: Save logs
if: always()
Expand Down
112 changes: 112 additions & 0 deletions src/internal/api/packages/tunnels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package packages provides api functions for managing Zarf packages.
package packages

import (
"errors"
"net/http"

"github.com/defenseunicorns/zarf/src/internal/api/common"
"github.com/defenseunicorns/zarf/src/internal/cluster"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/types"
"github.com/go-chi/chi/v5"
)

// PackageTunnel is a struct for storing a tunnel and its connection details
type PackageTunnel struct {
tunnel *cluster.Tunnel
Connection types.APIDeployedPackageConnection `json:"connection,omitempty"`
}

// packageTunnels is a map of package names to PackageTunnel objects
type packageTunnels map[string]map[string]PackageTunnel

// tunnels is a map of package names to tunnel objects used for storing connected tunnels
var tunnels = make(packageTunnels)

// ListConnections returns a map of pkgName to a list of connections
func ListConnections(w http.ResponseWriter, _ *http.Request) {
allConnections := make(types.APIConnections)
for name, pkgTunnels := range tunnels {
for _, pkgTunnel := range pkgTunnels {
if allConnections[name] == nil {
allConnections[name] = make(types.APIDeployedPackageConnections, 0)
}
allConnections[name] = append(allConnections[name], pkgTunnel.Connection)
}
}
common.WriteJSONResponse(w, allConnections, http.StatusOK)
}

// ListPackageConnections lists all tunnel names
func ListPackageConnections(w http.ResponseWriter, r *http.Request) {
pkgName := chi.URLParam(r, "pkg")
if tunnels[pkgName] == nil {
message.ErrorWebf(errors.New("No tunnels for package %s"), w, pkgName)
return
}
pkgTunnels := make(types.APIDeployedPackageConnections, 0, len(tunnels[pkgName]))
for _, pkgTunnel := range tunnels[pkgName] {
pkgTunnels = append(pkgTunnels, pkgTunnel.Connection)
}

common.WriteJSONResponse(w, pkgTunnels, http.StatusOK)
}

// ConnectTunnel establishes a tunnel for the requested resource
func ConnectTunnel(w http.ResponseWriter, r *http.Request) {
pkgName := chi.URLParam(r, "pkg")
connectionName := chi.URLParam(r, "name")

if tunnels[pkgName] == nil {
tunnels[pkgName] = make(map[string]PackageTunnel)
}

pkgTunnels := tunnels[pkgName]

if pkgTunnels[connectionName].tunnel != nil {
common.WriteJSONResponse(w, tunnels[pkgName][connectionName].Connection, http.StatusOK)
return
}

tunnel, err := cluster.NewZarfTunnel()

if err != nil {
message.ErrorWebf(err, w, "Failed to create tunnel for %s", connectionName)
return
}

err = tunnel.Connect(connectionName, false)
if err != nil {
message.ErrorWebf(err, w, "Failed to connect to %s", connectionName)
return
}

tunnels[pkgName][connectionName] = PackageTunnel{
tunnel: tunnel,
Connection: types.APIDeployedPackageConnection{
Name: connectionName,
URL: tunnel.FullURL(),
},
}
common.WriteJSONResponse(w, tunnels[pkgName][connectionName].Connection, http.StatusCreated)
}

// DisconnectTunnel closes the tunnel for the requested resource
func DisconnectTunnel(w http.ResponseWriter, r *http.Request) {
pkgName := chi.URLParam(r, "pkg")
connectionName := chi.URLParam(r, "name")
pkgTunnel := tunnels[pkgName][connectionName]
if pkgTunnel.tunnel == nil {
message.ErrorWebf(errors.New("Tunnel not found"), w, "Failed to disconnect from %s", connectionName)
return
}

pkgTunnel.tunnel.Close()
delete(tunnels[pkgName], connectionName)

common.WriteJSONResponse(w, true, http.StatusOK)
}
6 changes: 5 additions & 1 deletion src/internal/api/start.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package api provides the UI API server.
Expand Down Expand Up @@ -93,12 +94,15 @@ func LaunchAPIServer() {
r.Put("/deploy", packages.DeployPackage)
r.Get("/deploy-stream", packages.StreamDeployPackage)
r.Delete("/remove/{name}", packages.RemovePackage)
r.Put("/{pkg}/connect/{name}", packages.ConnectTunnel)
r.Delete("/{pkg}/disconnect/{name}", packages.DisconnectTunnel)
r.Get("/{pkg}/connections", packages.ListPackageConnections)
r.Get("/connections", packages.ListConnections)
})

r.Route("/components", func(r chi.Router) {
r.Get("/deployed", components.ListDeployingComponents)
})

})

// If no dev port specified, use the server port for the URL and try to open it
Expand Down
5 changes: 5 additions & 0 deletions src/internal/cluster/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ func (tunnel *Tunnel) HTTPEndpoint() string {
return fmt.Sprintf("http://%s", tunnel.Endpoint())
}

// FullURL returns the tunnel endpoint as a HTTP URL string with the urlSuffix appended.
func (tunnel *Tunnel) FullURL() string {
return fmt.Sprintf("%s%s", tunnel.HTTPEndpoint(), tunnel.urlSuffix)
}

// Close disconnects a tunnel connection by closing the StopChan, thereby stopping the goroutine.
func (tunnel *Tunnel) Close() {
message.Debug("tunnel.Close()")
Expand Down
3 changes: 2 additions & 1 deletion src/internal/cluster/zarf.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (c *Cluster) StripZarfLabelsAndSecretsFromNamespaces() {
}

// RecordPackageDeployment saves metadata about a package that has been deployed to the cluster.
func (c *Cluster) RecordPackageDeployment(pkg types.ZarfPackage, components []types.DeployedComponent) {
func (c *Cluster) RecordPackageDeployment(pkg types.ZarfPackage, components []types.DeployedComponent, connectStrings types.ConnectStrings) {
// Generate a secret that describes the package that is being deployed
packageName := pkg.Metadata.Name
deployedPackageSecret := c.Kube.GenerateSecret(ZarfNamespaceName, config.ZarfPackagePrefix+packageName, corev1.SecretTypeOpaque)
Expand All @@ -108,6 +108,7 @@ func (c *Cluster) RecordPackageDeployment(pkg types.ZarfPackage, components []ty
CLIVersion: config.CLIVersion,
Data: pkg,
DeployedComponents: components,
ConnectStrings: connectStrings,
})

deployedPackageSecret.Data = map[string][]byte{"data": stateData}
Expand Down
3 changes: 2 additions & 1 deletion src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ func (p *Packager) Deploy() error {

// Notify all the things about the successful deployment
message.Successf("Zarf deployment complete")

p.printTablesForDeployment(deployedComponents)

// Save deployed package information to k8s
// Note: Not all packages need k8s; check if k8s is being used before saving the secret
if p.cluster != nil {
p.cluster.RecordPackageDeployment(p.cfg.Pkg, deployedComponents)
p.cluster.RecordPackageDeployment(p.cfg.Pkg, deployedComponents, connectStrings)
}

return nil
Expand Down
77 changes: 77 additions & 0 deletions src/test/ui/04_connect_doom.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

import { expect, test } from '@playwright/test';

test.describe.serial('connect the dos-games package @connect', async () => {
test.beforeEach(async ({ page }) => {
page.on('pageerror', (err) => console.log(err.message));
await page.goto('/auth?token=insecure', { waitUntil: 'networkidle' });
});

test('connect the dos-games package', async ({ page }) => {
let menu = await openDosGamesMenu(page);

// Ensure the menu contains the Connect option
expect(await menu.textContent()).toContain('Connect...');

const connect = menu.locator('span:text-is("Connect...")').first();

// Open Connect Deployed Package Dialog
await connect.click();

const connectDialog = page.locator('.dialog-open');
expect(await connectDialog.textContent()).toContain('Connect to Resource');
const connectButton = connectDialog.locator('button:has-text("Connect")');

// Click the Connect Button
await Promise.all([
page.waitForResponse('api/packages/dos-games/connect/doom'),
connectButton.click(),
]);

menu = await openDosGamesMenu(page);
expect(await menu.textContent()).toContain('Disconnect...');
});

test('disconnect the dos-games package', async ({ page }) => {
// Dispose context once it's no longer needed.
let menu = await openDosGamesMenu(page);

// Ensure the menu contains the Disconnect option
expect(await menu.textContent()).toContain('Disconnect...');

const disconnect = menu.locator('span:text-is("Disconnect...")');

// Open Disconnect Deployed Package Dialog
await disconnect.click();

const dialog = page.locator('.dialog-open');
expect(await dialog.textContent()).toContain('Disconnect Resource');
const disconnectButton = dialog.locator('.button-label:text-is("Disconnect")');

// Click the Disconnect Button
await Promise.all([
page.waitForResponse('api/packages/dos-games/disconnect/doom'),
disconnectButton.click(),
]);

// Ensure the menu no longer contains the Disconnect option
menu = await openDosGamesMenu(page);
expect(await menu.textContent()).not.toContain('Disconnect...');
});
});

async function openDosGamesMenu(page: any) {
// Find Dos Games Package in Packages Table
const packageTableBody = page.locator('.package-list-body');
const packageRow = packageTableBody.locator('.package-table-row:has-text("dos-games")');

// Open the menu for the package
const more = packageRow.locator('.more > button').first();
await more.click();

// Find the menu and return it
const menu = page.locator('.menu.open');
return menu;
}
41 changes: 29 additions & 12 deletions src/types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@
// Package types contains all the types used by Zarf.
package types

import "k8s.io/client-go/tools/clientcmd/api"
import (
"k8s.io/client-go/tools/clientcmd/api"
)

// RestAPI is the struct that is used to marshal/unmarshal the top-level API objects.
type RestAPI struct {
ZarfPackage ZarfPackage `json:"zarfPackage"`
ZarfState ZarfState `json:"zarfState"`
ZarfCommonOptions ZarfCommonOptions `json:"zarfCommonOptions"`
ZarfCreateOptions ZarfCreateOptions `json:"zarfCreateOptions"`
ZarfDeployOptions ZarfDeployOptions `json:"zarfDeployOptions"`
ZarfInitOptions ZarfInitOptions `json:"zarfInitOptions"`
ConnectStrings ConnectStrings `json:"connectStrings"`
ClusterSummary ClusterSummary `json:"clusterSummary"`
DeployedPackage DeployedPackage `json:"deployedPackage"`
APIZarfPackage APIZarfPackage `json:"apiZarfPackage"`
APIZarfDeployPayload APIZarfDeployPayload `json:"apiZarfDeployPayload"`
ZarfPackage ZarfPackage `json:"zarfPackage"`
ZarfState ZarfState `json:"zarfState"`
ZarfCommonOptions ZarfCommonOptions `json:"zarfCommonOptions"`
ZarfCreateOptions ZarfCreateOptions `json:"zarfCreateOptions"`
ZarfDeployOptions ZarfDeployOptions `json:"zarfDeployOptions"`
ZarfInitOptions ZarfInitOptions `json:"zarfInitOptions"`
ConnectStrings ConnectStrings `json:"connectStrings"`
ClusterSummary ClusterSummary `json:"clusterSummary"`
DeployedPackage DeployedPackage `json:"deployedPackage"`
APIZarfPackage APIZarfPackage `json:"apiZarfPackage"`
APIZarfDeployPayload APIZarfDeployPayload `json:"apiZarfDeployPayload"`
APIZarfPackageConnection APIDeployedPackageConnection `json:"apiZarfPackageConnection"`
APIDeployedPackageConnections APIDeployedPackageConnections `json:"apiZarfPackageConnections"`
APIConnections APIConnections `json:"apiConnections"`
}

// ClusterSummary contains the summary of a cluster for the API.
Expand All @@ -42,3 +47,15 @@ type APIZarfDeployPayload struct {
DeployOpts ZarfDeployOptions `json:"deployOpts"`
InitOpts *ZarfInitOptions `json:"initOpts,omitempty"`
}

// APIConnections represents all of the existing connections
type APIConnections map[string]APIDeployedPackageConnections

// APIDeployedPackageConnections represents all of the connections for a deployed package
type APIDeployedPackageConnections []APIDeployedPackageConnection

// APIDeployedPackageConnection represents a single connection from a deployed package
type APIDeployedPackageConnection struct {
Name string `json:"name"`
URL string `json:"url,omitempty"`
}
1 change: 1 addition & 0 deletions src/types/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type DeployedPackage struct {
CLIVersion string `json:"cliVersion"`

DeployedComponents []DeployedComponent `json:"deployedComponents"`
ConnectStrings ConnectStrings `json:"connectStrings,omitempty"`
}

// DeployedComponent contains information about a Zarf Package Component that has been deployed to a cluster.
Expand Down
Loading

0 comments on commit 5fcdc3c

Please sign in to comment.