Skip to content

Commit

Permalink
Merge pull request #24 from NVIDIA/ghaction
Browse files Browse the repository at this point in the history
Go all GO
  • Loading branch information
ArangoGutierrez authored Mar 4, 2024
2 parents f0595fb + cafa43f commit 4a98fc8
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 89 deletions.
14 changes: 4 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,12 @@
## limitations under the License.
##

FROM golang:1.21-bookworm
FROM golang:1.22-bookworm

WORKDIR /src
COPY . .

RUN make build
RUN install -m 755 /src/bin/holodeck /usr/local/bin/holodeck && \
install -m 755 /src/scripts/run.sh /usr/local/bin/run.sh && \
install -m 755 /src/scripts/cleanup.sh /usr/local/bin/cleanup.sh
RUN make build-action
RUN install -m 755 /src/bin/holodeck /usr/local/bin/holodeck

RUN echo "nobody:x:65534:65534:Nobody:/:" >> /etc/passwd
# Run as unprivileged user
USER 65534:65534

ENTRYPOINT ["/bin/bash", "-c", "run.sh"]
ENTRYPOINT ["/usr/local/bin/holodeck"]
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ IMAGE_TAG := $(IMAGE_REPO):$(IMAGE_TAG_NAME)

PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))

build:
build-action:
@rm -rf bin
$(GO_CMD) build -o bin/$(BINARY_NAME) cmd/main.go
$(GO_CMD) build -o bin/$(BINARY_NAME) cmd/action/main.go

build-cli:
@rm -rf bin
$(GO_CMD) build -o bin/$(BINARY_NAME) cmd/cli/main.go

fmt:
@$(GO_FMT) -w -l $$(find . -name '*.go')
Expand Down
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ description: A tool for creating and managing GPU-ready cloud test environments.
runs:
using: docker
image: Dockerfile
entrypoint: 'run.sh'
post-entrypoint: 'cleanup.sh'
entrypoint: 'holodeck'
post-entrypoint: 'holodeck'

inputs:
aws_access_key_id:
Expand Down
53 changes: 53 additions & 0 deletions cmd/action/ci/ci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ci

import (
"os"

"github.com/NVIDIA/holodeck/internal/logger"
)

const (
cachedir = "/github/workspace/.cache"
cacheFile = "/github/workspace/.cache/holodeck.yaml"
kubeconfig = "/github/workspace/kubeconfig"
sshKeyFile = "/github/workspace/.cache/key"
)

func Run(log *logger.FunLogger) error {
log.Info("Running Holodeck function")
// Check if .cache folder exists in the /github/workspace directory and if it does, call cleanup function
// If it doesn't, call entrypoint function
// If the entrypoint function returns an error, call cleanup function
if _, err := os.Stat(cachedir); err == nil {
if err := cleanup(log); err != nil {
return err
}
} else {
if err := entrypoint(log); err != nil {
if err := cleanup(log); err != nil {
return err
}
return err
}
}

log.Check("Holodeck completed successfully")

return nil
}
79 changes: 79 additions & 0 deletions cmd/action/ci/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ci

import (
"fmt"
"os"

"github.com/NVIDIA/holodeck/api/holodeck/v1alpha1"
"github.com/NVIDIA/holodeck/internal/logger"
"github.com/NVIDIA/holodeck/pkg/jyaml"
"github.com/NVIDIA/holodeck/pkg/provider/aws"
"k8s.io/klog/v2"
)

func cleanup(log *logger.FunLogger) error {
klog.Info("Running Cleanup function")

configFile := os.Getenv("INPUT_HOLODECK_CONFIG")
if configFile == "" {
log.Error(fmt.Errorf("config file not provided"))
os.Exit(1)
}

// Read the config file
cfg, err := jyaml.UnmarshalFromFile[v1alpha1.Environment](configFile)
if err != nil {
return fmt.Errorf("error reading config file: %s", err)
}

client, err := aws.New(log, cfg, cacheFile)
if err != nil {
log.Error(err)
log.Exit(1)
}

// check if cache exists
if _, err := os.Stat(cacheFile); err != nil {
fmt.Printf("Error reading cache file: %s\n", err)
fmt.Printf("Cache file %s does not exist\n", cacheFile)
os.Exit(1)
}

if err := client.Delete(); err != nil {
log.Error(err)
log.Exit(1)
}

// Delete the cache kubeconfig and ssh key
if err := os.Remove(kubeconfig); err != nil {
log.Error(fmt.Errorf("error deleting kubeconfig: %s", err))
}

if err := os.Remove(sshKeyFile); err != nil {
log.Error(fmt.Errorf("error deleting ssh key: %s", err))
}

if err := os.RemoveAll(cachedir); err != nil {
log.Error(fmt.Errorf("error deleting cache directory: %s", err))
}

log.Info("Successfully deleted environment %s\n", cfg.Name)

return nil
}
189 changes: 189 additions & 0 deletions cmd/action/ci/entrypoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ci

import (
"fmt"
"io"
"os"

"github.com/NVIDIA/holodeck/api/holodeck/v1alpha1"
"github.com/NVIDIA/holodeck/internal/logger"
"github.com/NVIDIA/holodeck/pkg/jyaml"
"github.com/NVIDIA/holodeck/pkg/provider/aws"
"github.com/NVIDIA/holodeck/pkg/provisioner"
)

func entrypoint(log *logger.FunLogger) error {
log.Info("Running Entrypoint function")

configFile := os.Getenv("INPUT_HOLODECK_CONFIG")
if configFile == "" {
log.Error(fmt.Errorf("config file not provided"))
os.Exit(1)
}

// Get INPUT_AWS_SSH_KEY and write it to a file
sshKey := os.Getenv("INPUT_AWS_SSH_KEY")
if sshKey == "" {
log.Error(fmt.Errorf("ssh key not provided"))
os.Exit(1)
}

err := os.WriteFile(sshKeyFile, []byte(sshKey), 0600)
if err != nil {
log.Error(fmt.Errorf("error writing ssh key to file: %s", err))
os.Exit(1)
}

// Map INPUT_AWS_ACCESS_KEY_ID and INPUT_AWS_SECRET_ACCESS_KEY
// to AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
accessKeyID := os.Getenv("INPUT_AWS_ACCESS_KEY_ID")
if accessKeyID == "" {
log.Error(fmt.Errorf("aws access key id not provided"))
os.Exit(1)
}

secretAccessKey := os.Getenv("INPUT_AWS_SECRET_ACCESS_KEY")
if secretAccessKey == "" {
log.Error(fmt.Errorf("aws secret access key not provided"))
os.Exit(1)
}

os.Setenv("AWS_ACCESS_KEY_ID", accessKeyID)
os.Setenv("AWS_SECRET_ACCESS_KEY", secretAccessKey)

// Read the config file
cfg, err := jyaml.UnmarshalFromFile[v1alpha1.Environment](configFile)
if err != nil {
return fmt.Errorf("error reading config file: %s", err)
}

// If no containerruntime is specified, default to none
if cfg.Spec.ContainerRuntime.Name == "" {
cfg.Spec.ContainerRuntime.Name = v1alpha1.ContainerRuntimeNone
}

// Set env name
sha := os.Getenv("GITHUB_SHA")
// short sha
if len(sha) > 8 {
sha = sha[:8]
}
if sha == "" {
log.Error(fmt.Errorf("github sha not provided"))
os.Exit(1)
}
cfg.SetName(sha)

client, err := aws.New(log, cfg, cacheFile)
if err != nil {
return err
}

err = client.Create()
if err != nil {
return err
}

// Read cache after creating the environment
cache, err := jyaml.UnmarshalFromFile[v1alpha1.Environment](cacheFile)
if err != nil {
return fmt.Errorf("failed to read cache file: %v", err)
}

var hostUrl string

log.Info("Provisioning \u2699")

for _, p := range cache.Status.Properties {
if p.Name == aws.PublicDnsName {
hostUrl = p.Value
break
}
}

p, err := provisioner.New(log, cfg.Spec.Auth.PrivateKey, cfg.Spec.Auth.Username, hostUrl)
if err != nil {
return err
}

if err = p.Run(cfg); err != nil {
return fmt.Errorf("failed to run provisioner: %v", err)
}

if cfg.Spec.Kubernetes.Install {
err = getKubeConfig(log, &cfg, hostUrl)
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %v", err)
}
}

return nil
}

// getKubeConfig downloads the kubeconfig file from the remote host
func getKubeConfig(log *logger.FunLogger, cfg *v1alpha1.Environment, hostUrl string) error {
remoteFilePath := "/home/ubuntu/.kube/config"

// Create a new ssh session
p, err := provisioner.New(log, cfg.Spec.Auth.PrivateKey, cfg.Spec.Auth.Username, hostUrl)
if err != nil {
return err
}

session, err := p.Client.NewSession()
if err != nil {
return fmt.Errorf("error creating session: %v", err)
}
defer session.Close()

// Set up a pipe to receive the remote file content
remoteFile, err := session.StdoutPipe()
if err != nil {
return fmt.Errorf("error creating remote file pipe: %v", err)
}

// Start the remote command to read the file content
err = session.Start(fmt.Sprintf("/usr/bin/cat %s", remoteFilePath))
if err != nil {
return fmt.Errorf("error starting remote command: %v", err)
}

// Create a new file on the local system to save the downloaded content
localFile, err := os.Create(kubeconfig)
if err != nil {
return fmt.Errorf("error creating local file: %v", err)
}
defer localFile.Close()

// Copy the remote file content to the local file
_, err = io.Copy(localFile, remoteFile)
if err != nil {
return fmt.Errorf("error copying remote file to local: %v", err)
}

// Wait for the remote command to finish
err = session.Wait()
if err != nil {
return fmt.Errorf("error waiting for remote command: %v", err)
}

log.Info(fmt.Sprintf("Kubeconfig saved to %s\n", kubeconfig))

return nil
}
Loading

0 comments on commit 4a98fc8

Please sign in to comment.