Skip to content

Commit

Permalink
Merge pull request #1 from NVIDIA/dryrun
Browse files Browse the repository at this point in the history
Add Dryrun sub cmd
  • Loading branch information
ArangoGutierrez authored Jan 22, 2024
2 parents 1ff2836 + 6b4d9b9 commit 3d80bd6
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 25 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,21 @@ spec:
### Create an environment
```bash
holodeck create -f ./examples/v1alpha1_environment.yaml
$ holodeck create -f ./examples/v1alpha1_environment.yaml
```

### Delete an environment

```bash
holodeck delete -f ./examples/v1alpha1_environment.yaml
$ holodeck delete -f ./examples/v1alpha1_environment.yaml
```

### Dry Run

```bash
$ holodeck dryrun -f ./examples/v1alpha1_environment.yaml
Checking if instance type g4dn.xlarge is supported in region xx-xxxx-1
Checking if image ami-xxxxxxxxx is supported in region xx-xxxx-1
Resolving dependencies...
Dryrun succeeded
```
5 changes: 3 additions & 2 deletions cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type command struct {
logger *logrus.Logger
}

// NewCommand constructs a validate command with the specified logger
// NewCommand constructs the create command with the specified logger
func NewCommand(logger *logrus.Logger) *cli.Command {
c := command{
logger: logger,
Expand All @@ -58,7 +58,7 @@ func NewCommand(logger *logrus.Logger) *cli.Command {
func (m command) build() *cli.Command {
opts := options{}

// Create the 'validate' command
// Create the 'create' command
create := cli.Command{
Name: "create",
Usage: "create a test environment based on config file",
Expand Down Expand Up @@ -170,6 +170,7 @@ func runProvision(opts *options) error {
return nil
}

// getKubeConfig downloads the kubeconfig file from the remote host
func getKubeConfig(opts *options, p *provisioner.Provisioner) error {
remoteFilePath := "/home/ubuntu/.kube/config"
if opts.cfg.Spec.Kubernetes.KubeConfig == "" {
Expand Down
4 changes: 2 additions & 2 deletions cmd/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type command struct {
logger *logrus.Logger
}

// NewCommand constructs a validate command with the specified logger
// NewCommand constructs the delete command with the specified logger
func NewCommand(logger *logrus.Logger) *cli.Command {
c := command{
logger: logger,
Expand All @@ -54,7 +54,7 @@ func NewCommand(logger *logrus.Logger) *cli.Command {
func (m command) build() *cli.Command {
opts := options{}

// Create the 'validate' command
// Create the 'delete' command
create := cli.Command{
Name: "delete",
Usage: "delete a test environment",
Expand Down
123 changes: 123 additions & 0 deletions cmd/dryrun/dryrun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2023, 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 dryrun

import (
"fmt"

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

"github.com/sirupsen/logrus"
cli "github.com/urfave/cli/v2"
)

type options struct {
envFile string

cfg v1alpha1.Environment
}

type command struct {
logger *logrus.Logger
}

// NewCommand constructs a dryrun command with the specified logger
func NewCommand(logger *logrus.Logger) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}

func (m command) build() *cli.Command {
opts := options{}

// Create the 'dryrun' command
dryrun := cli.Command{
Name: "dryrun",
Usage: "dryrun a test environment based on config file",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "envFile",
Aliases: []string{"f"},
Usage: "Path to the Environment file",
Destination: &opts.envFile,
},
},
Before: func(c *cli.Context) error {
// Read the config file
var err error
opts.cfg, err = jyaml.UnmarshalFromFile[v1alpha1.Environment](opts.envFile)
if err != nil {
fmt.Printf("failed to read config file: %v\n", err)
return err
}

return nil
},
Action: func(c *cli.Context) error {
return m.run(c, &opts)
},
}

return &dryrun
}

func (m command) run(c *cli.Context, opts *options) error {
// Check Provider
switch opts.cfg.Spec.Provider {
case v1alpha1.ProviderAWS:
err := validateAWS(opts)
if err != nil {
return err
}
case v1alpha1.ProviderSSH:
// Creating a new provisioner will validate the private key and hostUrl
_, err := provisioner.New(opts.cfg.Spec.Auth.PrivateKey, opts.cfg.Spec.Instance.HostUrl)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown provider %s", opts.cfg.Spec.Provider)
}

// Check Provisioner
err := provisioner.Dryrun(opts.cfg)
if err != nil {
return err
}

fmt.Printf("Dryrun succeeded\n")

return nil
}

func validateAWS(opts *options) error {
client, err := aws.New(opts.cfg, opts.envFile)
if err != nil {
return err
}

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

return nil
}
2 changes: 2 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/NVIDIA/holodeck/cmd/create"
"github.com/NVIDIA/holodeck/cmd/delete"
"github.com/NVIDIA/holodeck/cmd/dryrun"

log "github.com/sirupsen/logrus"
cli "github.com/urfave/cli/v2"
Expand Down Expand Up @@ -71,6 +72,7 @@ func main() {
c.Commands = []*cli.Command{
create.NewCommand(logger),
delete.NewCommand(logger),
dryrun.NewCommand(logger),
}

err := c.Run(os.Args)
Expand Down
73 changes: 54 additions & 19 deletions pkg/provider/aws/dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,74 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
)

func (a *Client) getInstanceTypes() ([]types.InstanceType, error) {
// Use the DescribeInstanceTypes API to get a list of supported instance types in the current region
resp, err := a.ec2.DescribeInstanceTypes(context.TODO(), &ec2.DescribeInstanceTypesInput{})
if err != nil {
return nil, err
}
func (a *Client) checkInstanceTypes() error {
var nextToken *string

for {
// Use the DescribeInstanceTypes API to get a list of supported instance types in the current region
resp, err := a.ec2.DescribeInstanceTypes(context.TODO(), &ec2.DescribeInstanceTypesInput{NextToken: nextToken})
if err != nil {
return err
}

instanceTypes := []types.InstanceType{}
for _, it := range resp.InstanceTypes {
instanceTypes = append(instanceTypes, it.InstanceType)
for _, it := range resp.InstanceTypes {
if it.InstanceType == types.InstanceType(a.Spec.Instance.Type) {
return nil
}
}

if resp.NextToken != nil {
nextToken = resp.NextToken
} else {
break
}
}

return instanceTypes, nil
return fmt.Errorf("instance type %s is not supported in the current region %s", string(a.Spec.Instance.Type), a.Spec.Instance.Region)
}

func (a *Client) isInstanceTypeSupported(desiredType string, supportedTypes []types.InstanceType) bool {
for _, t := range supportedTypes {
if t == types.InstanceType(a.Spec.Instance.Type) {
return true
func (a *Client) checkImages() error {
var nextToken *string

for {
// Use the DescribeImages API to get a list of supported images in the current region
resp, err := a.ec2.DescribeImages(context.TODO(), &ec2.DescribeImagesInput{
NextToken: nextToken,
},
)
if err != nil {
return err
}

for _, image := range resp.Images {
if *image.ImageId == *a.Spec.Instance.Image.ImageId {
return nil
}
}

if resp.NextToken != nil {
nextToken = resp.NextToken
} else {
break
}
}
return false

return fmt.Errorf("image %s is not supported in the current region %s", *a.Spec.Instance.Image.ImageId, a.Spec.Instance.Region)
}

func (a *Client) DryRun() error {
// Check if the desired instance type is supported in the region
instanceTypes, err := a.getInstanceTypes()
fmt.Printf("Checking if instance type %s is supported in region %s\n", string(a.Spec.Instance.Type), a.Spec.Instance.Region)
err := a.checkInstanceTypes()
if err != nil {
return fmt.Errorf("failed to get instance types: %v", err)
return err
}

if !a.isInstanceTypeSupported(string(a.Spec.Instance.Type), instanceTypes) {
return fmt.Errorf("instance type %s is not supported in the current region %s", string(a.Spec.Instance.Type), a.Spec.Instance.Region)
// Check if the desired image is supported in the region
fmt.Printf("Checking if image %s is supported in region %s\n", *a.Spec.Instance.Image.ImageId, a.Spec.Instance.Region)
err = a.checkImages()
if err != nil {
return fmt.Errorf("failed to get images: %v", err)
}

return nil
Expand Down
54 changes: 54 additions & 0 deletions pkg/provisioner/dryrun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2023, 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 provisioner

import (
"fmt"
"strings"

"github.com/NVIDIA/holodeck/api/holodeck/v1alpha1"
)

func Dryrun(env v1alpha1.Environment) error {
// Resolve dependencies from top to bottom
fmt.Printf("Resolving dependencies...\n")
// kubernetes -> container runtime -> node
if env.Spec.Kubernetes.Install {
if !env.Spec.ContainerRuntime.Install {
return fmt.Errorf("cannot install Kubernetes without a container runtime")
}
// check if env.Spec.Kubernetes.KubernetesVersion is in the format of vX.Y.Z
if !strings.HasPrefix(env.Spec.Kubernetes.KubernetesVersion, "v") {
return fmt.Errorf("Kubernetes version %s is not in the format of vX.Y.Z", env.Spec.Kubernetes.KubernetesVersion)
}
}

if env.Spec.ContainerRuntime.Install && (env.Spec.ContainerRuntime.Name != v1alpha1.ContainerRuntimeContainerd &&
env.Spec.ContainerRuntime.Name != v1alpha1.ContainerRuntimeCrio &&
env.Spec.ContainerRuntime.Name != v1alpha1.ContainerRuntimeDocker) {
return fmt.Errorf("container runtime %s not supported", env.Spec.ContainerRuntime.Name)
}

if env.Spec.NVContainerToolKit.Install && !env.Spec.ContainerRuntime.Install {
return fmt.Errorf("cannot install NVContainer Toolkit without a container runtime")
}
if env.Spec.NVContainerToolKit.Install && !env.Spec.NVDriver.Install {
return fmt.Errorf("cannot install NVContainer Toolkit without the NVIDIA driver")
}

return nil
}

0 comments on commit 3d80bd6

Please sign in to comment.