Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add container exec to cyclops UI #526

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cyclops-ctrl/internal/cluster/k8sclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -916,3 +916,39 @@ func isSecret(group, version, kind string) bool {
func isCronJob(group, version, kind string) bool {
return group == "batch" && version == "v1" && kind == "CronJob"
}

func WatchResource(clientset *kubernetes.Clientset, group, version, kind, name, namespace string) (<-chan watch.Event, error) {
// Create a channel to publish events
eventChannel := make(chan watch.Event)

// Define a context
ctx, cancel := context.WithCancel(context.Background())

// Create a dynamic client for the specified resource
dynamicClient, err := dynamic.NewForConfig(clientset.RESTConfig())
if err != nil {
return nil, err
}

resource := schema.GroupVersionResource{Group: group, Version: version, Resource: kind}

// Start the watch
watch, err := dynamicClient.Resource(resource).Namespace(namespace).Watch(ctx, metav1.ListOptions{
FieldSelector: "metadata.name=" + name,
})
if err != nil {
cancel()
return nil, err
}

// Goroutine to handle watch events
go func() {
defer close(eventChannel)
defer cancel()
for event := range watch.ResultChan() {
eventChannel <- event
}
}()

return eventChannel, nil
}
66 changes: 47 additions & 19 deletions cyclops-ctrl/internal/template/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
"github.com/pkg/errors"

"github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/models"
"github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/auth"
)

func (r Repo) LoadOCIHelmChart(repo, chart, version string) (*models.Template, error) {
func (r Repo) LoadOCIHelmChart(repo, chart, version string, creds *auth.Credentials) (*models.Template, error) {
var err error
strictVersion := version
if !isValidVersion(version) {
strictVersion, err = getOCIStrictVersion(repo, chart, version)
strictVersion, err = getOCIStrictVersion(repo, chart, version, creds)
if err != nil {
return nil, err
}
Expand All @@ -29,7 +30,7 @@ func (r Repo) LoadOCIHelmChart(repo, chart, version string) (*models.Template, e
}

var tgzData []byte
tgzData, err = loadOCIHelmChartBytes(repo, chart, version)
tgzData, err = loadOCIHelmChartBytes(repo, chart, version, creds)
if err != nil {
return nil, err
}
Expand All @@ -52,11 +53,11 @@ func (r Repo) LoadOCIHelmChart(repo, chart, version string) (*models.Template, e
return template, nil
}

func (r Repo) LoadOCIHelmChartInitialValues(repo, chart, version string) (map[string]interface{}, error) {
func (r Repo) LoadOCIHelmChartInitialValues(repo, chart, version string, creds *auth.Credentials ) (map[string]interface{}, error) {
var err error
strictVersion := version
if !isValidVersion(version) {
strictVersion, err = getOCIStrictVersion(repo, chart, version)
strictVersion, err = getOCIStrictVersion(repo, chart, version, creds)
if err != nil {
return nil, err
}
Expand All @@ -67,7 +68,7 @@ func (r Repo) LoadOCIHelmChartInitialValues(repo, chart, version string) (map[st
return cached, nil
}

tgzData, err := loadOCIHelmChartBytes(repo, chart, version)
tgzData, err := loadOCIHelmChartBytes(repo, chart, version, creds)
if err != nil {
return nil, err
}
Expand All @@ -87,34 +88,34 @@ func (r Repo) LoadOCIHelmChartInitialValues(repo, chart, version string) (map[st
return initial, nil
}

func loadOCIHelmChartBytes(repo, chart, version string) ([]byte, error) {
func loadOCIHelmChartBytes(repo, chart, version string, creds *auth.Credentials) ([]byte, error) {
var err error
if !isValidVersion(version) {
version, err = getOCIStrictVersion(repo, chart, version)
version, err = getOCIStrictVersion(repo, chart, version, creds)
if err != nil {
return nil, err
}
}

token, err := authorizeOCI(repo, chart, version)
token, err := authorizeOCI(repo, chart, version, creds)
if err != nil {
return nil, err
}

digest, err := fetchDigest(repo, chart, version, token)
digest, err := fetchDigest(repo, chart, version, token, creds)
if err != nil {
return nil, err
}

contentDigest, err := fetchContentDigest(repo, chart, digest, token)
contentDigest, err := fetchContentDigest(repo, chart, digest, token, creds)
if err != nil {
return nil, err
}

return loadOCITar(repo, chart, contentDigest, token)
return loadOCITar(repo, chart, contentDigest, token, creds)
}

func loadOCITar(repo, chart, digest, token string) ([]byte, error) {
func loadOCITar(repo, chart, digest, token string, creds *auth.Credentials) ([]byte, error) {
bURL, err := blobURL(repo, chart, digest)
if err != nil {
return nil, err
Expand All @@ -129,6 +130,8 @@ func loadOCITar(repo, chart, digest, token string) ([]byte, error) {
req.Header.Set("Accept", "application/vnd.cncf.helm.config.v1+json, */*")
if len(token) != 0 {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
} else if auth := httpBasicAuthCredentials(creds); auth != nil { // MODIFIED: Apply basic auth if no token
req.SetBasicAuth(auth.Username, auth.Password)
}

client := &http.Client{}
Expand All @@ -141,7 +144,7 @@ func loadOCITar(repo, chart, digest, token string) ([]byte, error) {
return ioutil.ReadAll(resp.Body)
}

func fetchContentDigest(repo, chart, digest, token string) (string, error) {
func fetchContentDigest(repo, chart, digest, token string, creds *auth.Credentials) (string, error) {
dURL, err := contentDigestURL(repo, chart, digest)
if err != nil {
return "", err
Expand All @@ -156,6 +159,8 @@ func fetchContentDigest(repo, chart, digest, token string) (string, error) {
req.Header.Set("Accept", "application/vnd.oci.image.manifest.v1+json, */*")
if len(token) != 0 {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
} else if auth := httpBasicAuthCredentials(creds); auth != nil { // MODIFIED: Apply basic auth if no token
req.SetBasicAuth(auth.Username, auth.Password)
}

client := &http.Client{}
Expand Down Expand Up @@ -187,7 +192,7 @@ func fetchContentDigest(repo, chart, digest, token string) (string, error) {
return ct.Layers[0].Digest, nil
}

func fetchDigest(repo, chart, version, token string) (string, error) {
func fetchDigest(repo, chart, version, token string, creds *auth.Credentials) (string, error) {
dURL, err := digestURL(repo, chart, version)
if err != nil {
return "", err
Expand All @@ -202,6 +207,8 @@ func fetchDigest(repo, chart, version, token string) (string, error) {
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, */*")
if len(token) != 0 {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
} else if auth := httpBasicAuthCredentials(creds); auth != nil { // MODIFIED: Apply basic auth if no token
req.SetBasicAuth(auth.Username, auth.Password)
}

client := &http.Client{}
Expand All @@ -214,8 +221,8 @@ func fetchDigest(repo, chart, version, token string) (string, error) {
return resp.Header.Get("docker-content-digest"), nil
}

func getOCIStrictVersion(repo, chart, version string) (string, error) {
token, err := authorizeOCITags(repo, chart)
func getOCIStrictVersion(repo, chart, version string, creds *auth.Credentials) (string, error) {
token, err := authorizeOCITags(repo, chart, creds)
if err != nil {
return "", err
}
Expand All @@ -233,6 +240,8 @@ func getOCIStrictVersion(repo, chart, version string) (string, error) {
req.Header.Set("User-Agent", "Helm/3.13.3")
if len(token) != 0 {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
} else if auth := httpBasicAuthCredentials(creds); auth != nil { // MODIFIED: Apply basic auth if no token
req.SetBasicAuth(auth.Username, auth.Password)
}

client := &http.Client{}
Expand All @@ -258,7 +267,7 @@ func getOCIStrictVersion(repo, chart, version string) (string, error) {
return resolveSemver(version, tagsResp.Tags)
}

func authorizeOCI(repo, chart, version string) (string, error) {
func authorizeOCI(repo, chart, version string, creds *auth.Credentials) (string, error) {
// region head
dURL, err := digestURL(repo, chart, version)
if err != nil {
Expand All @@ -272,6 +281,10 @@ func authorizeOCI(repo, chart, version string) (string, error) {
return "", err
}

if auth := httpBasicAuthCredentials(creds); auth != nil { // MODIFIED: Apply basic auth
req.SetBasicAuth(auth.Username, auth.Password)
}

resp, err := client.Do(req)
if err != nil {
return "", err
Expand Down Expand Up @@ -332,7 +345,7 @@ func authorizeOCI(repo, chart, version string) (string, error) {
// endregion
}

func authorizeOCITags(repo, chart string) (string, error) {
func authorizeOCITags(repo, chart string, creds *auth.Credentials) (string, error) {
// region head
tURL, err := tagsURL(repo, chart)
if err != nil {
Expand All @@ -346,6 +359,10 @@ func authorizeOCITags(repo, chart string) (string, error) {
return "", err
}

if auth := httpBasicAuthCredentials(creds); auth != nil { // MODIFIED: Apply basic auth
req.SetBasicAuth(auth.Username, auth.Password)
}

resp, err := client.Do(req)
if err != nil {
return "", err
Expand Down Expand Up @@ -481,3 +498,14 @@ func tagsURL(repo, chart string) (*url.URL, error) {
Path: fmt.Sprintf("v2/%v/%v/tags/list", repoURL.Path, chart),
}, nil
}

func httpBasicAuthCredentials(creds *auth.Credentials) *http.BasicAuth {
if creds == nil {
return nil
}

return &http.BasicAuth{
Username: creds.Username,
Password: creds.Password,
}
}
82 changes: 82 additions & 0 deletions cyclops-ui/src/components/k8s-resources/common/ExecModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useEffect, useRef } from 'react';
import { Modal } from 'antd';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css'; // Ensure you include xterm CSS for styling

interface ExecModalProps {
visible: boolean;
onCancel: () => void;
podName: string;
containerName: string;
namespace: string;
}

const ExecModal: React.FC<ExecModalProps> = ({
visible,
onCancel,
podName,
containerName,
namespace,
}) => {
const terminalRef = useRef<HTMLDivElement | null>(null);
const xtermRef = useRef<Terminal | null>(null);

useEffect(() => {
if (visible && terminalRef.current) {
// Initialize the terminal
const terminal = new Terminal();
terminal.open(terminalRef.current);
xtermRef.current = terminal;

// Connect to the backend via WebSocket
const ws = new WebSocket(`wss://your-backend-url/exec?pod=${podName}&container=${containerName}&namespace=${namespace}`);

ws.onopen = () => {
console.log('WebSocket connection established');
terminal.write('Welcome to the terminal!\r\n');
};

ws.onmessage = (event) => {
terminal.write(event.data);
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
terminal.write('Error connecting to WebSocket.\r\n');
};

ws.onclose = () => {
console.log('WebSocket connection closed');
terminal.write('WebSocket connection closed.\r\n');
};

terminal.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
});

return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
terminal.dispose();
};
}
}, [visible, podName, containerName, namespace]);

return (
<Modal
title="Terminal"
visible={visible}
onCancel={onCancel}
footer={null}
width="80%"
bodyStyle={{ padding: 0 }}
>
<div ref={terminalRef} style={{ height: '500px' }}></div>
</Modal>
);
};

export default ExecModal;
Loading