Skip to content

Add support for exporting APIs #1368

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

Merged
merged 26 commits into from
Sep 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dfc7110
Cortex export
vishalbollu Sep 4, 2020
b65228d
Split APIID into multiple ids
vishalbollu Sep 15, 2020
b8bd572
Merge branch 'master' into refactor-ids
vishalbollu Sep 15, 2020
28ecb55
Self review
vishalbollu Sep 15, 2020
6b328ee
Merge branch 'refactor-ids' of github.com:cortexlabs/cortex into refa…
vishalbollu Sep 15, 2020
ba8553d
Merge branch 'master' into refactor-ids
deliahu Sep 15, 2020
5f59385
Merge branch 'master' into cx-export
vishalbollu Sep 15, 2020
ee08f13
Merge branch 'refactor-ids' into cx-export
vishalbollu Sep 15, 2020
e6d05f2
Omitifempty for optional fields and inline resource embedded struct
vishalbollu Sep 15, 2020
bd100cc
Merge branch 'master' into cx-export
vishalbollu Sep 17, 2020
647075a
Remove debugs and more omitempty
vishalbollu Sep 18, 2020
e370873
Use user provided yaml
vishalbollu Sep 18, 2020
f2c428e
Revert json flag
vishalbollu Sep 18, 2020
d10be76
Clean up PR
vishalbollu Sep 18, 2020
98e1e52
Update export.go
vishalbollu Sep 18, 2020
8537d59
Update validations.go
vishalbollu Sep 18, 2020
fa7827b
Merge branch 'master' into cx-export
vishalbollu Sep 23, 2020
67a344d
Merge branch 'master' into cx-export
vishalbollu Sep 25, 2020
4b884d8
Move export to cluster command
vishalbollu Sep 25, 2020
9f8bc15
Cleanup PR
vishalbollu Sep 25, 2020
8e449d1
Update lib_http_client.go
vishalbollu Sep 25, 2020
e7db60a
Respond to PR comments
vishalbollu Sep 26, 2020
eec4254
Update cluster.go
vishalbollu Sep 26, 2020
196464a
Update cluster.go
vishalbollu Sep 26, 2020
10c770b
Merge branch 'master' into cx-export
vishalbollu Sep 26, 2020
69ebfe1
Merge branch 'master' into cx-export
vishalbollu Sep 26, 2020
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
3 changes: 3 additions & 0 deletions cli/cluster/lib_http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ func (client *OperatorClient) MakeRequest(operatorConfig OperatorConfig, request

response, err := client.Do(request)
if err != nil {
if operatorConfig.EnvName == "" {
return nil, errors.Wrap(err, "failed to connect to operator", operatorConfig.OperatorEndpoint)
}
return nil, ErrorFailedToConnectOperator(err, operatorConfig.EnvName, operatorConfig.OperatorEndpoint)
}
defer response.Body.Close()
Expand Down
144 changes: 143 additions & 1 deletion cli/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cmd
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
Expand All @@ -27,6 +28,7 @@ import (
"github.com/cortexlabs/cortex/cli/cluster"
"github.com/cortexlabs/cortex/cli/types/cliconfig"
"github.com/cortexlabs/cortex/pkg/consts"
"github.com/cortexlabs/cortex/pkg/lib/archive"
"github.com/cortexlabs/cortex/pkg/lib/aws"
cr "github.com/cortexlabs/cortex/pkg/lib/configreader"
"github.com/cortexlabs/cortex/pkg/lib/console"
Expand All @@ -43,6 +45,8 @@ import (
"github.com/cortexlabs/cortex/pkg/types"
"github.com/cortexlabs/cortex/pkg/types/clusterconfig"
"github.com/cortexlabs/cortex/pkg/types/clusterstate"
"github.com/cortexlabs/cortex/pkg/types/spec"
"github.com/cortexlabs/cortex/pkg/types/userconfig"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -89,6 +93,11 @@ func clusterInit() {
addAWSCredentials(_downCmd)
_downCmd.Flags().BoolVarP(&_flagClusterDisallowPrompt, "yes", "y", false, "skip prompts")
_clusterCmd.AddCommand(_downCmd)

_exportCmd.Flags().SortFlags = false
addClusterConfigFlag(_exportCmd)
addAWSCredentials(_exportCmd)
_clusterCmd.AddCommand(_exportCmd)
}

func addClusterConfigFlag(cmd *cobra.Command) {
Expand Down Expand Up @@ -269,7 +278,7 @@ var _upCmd = &cobra.Command{
exit.Error(errors.Append(err, fmt.Sprintf("\n\nunable to locate operator load balancer; you can attempt to resolve this issue and configure your CLI environment by running `cortex cluster info --env %s`", _flagClusterEnv)))
}
if loadBalancer == nil {
exit.Error(ErrorNoOperatorLoadBalancer(_flagClusterEnv))
exit.Error(errors.Append(ErrorNoOperatorLoadBalancer(), fmt.Sprintf("; you can attempt to resolve this issue and configure your CLI environment by running `cortex cluster info --env %s`", _flagClusterEnv)))
}

newEnvironment := cliconfig.Environment{
Expand Down Expand Up @@ -434,6 +443,7 @@ var _downCmd = &cobra.Command{
if err != nil {
exit.Error(err)
}

warnIfNotAdmin(awsClient)

clusterState, err := clusterstate.GetClusterState(awsClient, accessConfig)
Expand Down Expand Up @@ -533,6 +543,138 @@ var _downCmd = &cobra.Command{
},
}

var _exportCmd = &cobra.Command{
Use: "export",
Short: "download the code and configuration for all APIs deployed in a cluster",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
telemetry.Event("cli.cluster.export")

if _flagClusterConfig != "" {
// Deprecation: specifying aws creds in cluster configuration is no longer supported
if err := detectAWSCredsInConfigFile(cmd.Use, _flagClusterConfig); err != nil {
exit.Error(err)
}
}

accessConfig, err := getClusterAccessConfig(_flagClusterDisallowPrompt)
if err != nil {
exit.Error(err)
}

awsCreds, err := awsCredentialsForManagingCluster(*accessConfig, _flagClusterDisallowPrompt)
if err != nil {
exit.Error(err)
}

// Check AWS access
awsClient, err := newAWSClient(*accessConfig.Region, awsCreds)
if err != nil {
exit.Error(err)
}
warnIfNotAdmin(awsClient)

clusterState, err := clusterstate.GetClusterState(awsClient, accessConfig)
if err != nil {
exit.Error(err)
}

err = clusterstate.AssertClusterStatus(*accessConfig.ClusterName, *accessConfig.Region, clusterState.Status, clusterstate.StatusCreateComplete)
if err != nil {
exit.Error(err)
}

loadBalancer, err := awsClient.FindLoadBalancer(map[string]string{
clusterconfig.ClusterNameTag: *accessConfig.ClusterName,
"cortex.dev/load-balancer": "operator",
})
if err != nil {
exit.Error(err)
}
if loadBalancer == nil {
exit.Error(ErrorNoOperatorLoadBalancer())
}

operatorConfig := cluster.OperatorConfig{
Telemetry: isTelemetryEnabled(),
ClientID: clientID(),
AWSAccessKeyID: awsCreds.AWSAccessKeyID,
AWSSecretAccessKey: awsCreds.AWSSecretAccessKey,
OperatorEndpoint: "https://" + *loadBalancer.DNSName,
}

info, err := cluster.Info(operatorConfig)
if err != nil {
exit.Error(err)
}

apisResponse, err := cluster.GetAPIs(operatorConfig)
if err != nil {
exit.Error(err)
}

var apiSpecs []spec.API

for _, batchAPI := range apisResponse.BatchAPIs {
apiSpecs = append(apiSpecs, batchAPI.Spec)
}

for _, realtimeAPI := range apisResponse.RealtimeAPIs {
apiSpecs = append(apiSpecs, realtimeAPI.Spec)
}

for _, trafficSplitter := range apisResponse.TrafficSplitters {
apiSpecs = append(apiSpecs, trafficSplitter.Spec)
}

if len(apiSpecs) == 0 {
fmt.Println(fmt.Sprintf("no apis found in cluster named %s in %s", *accessConfig.ClusterName, *accessConfig.Region))
exit.Ok()
}

exportPath := fmt.Sprintf("export-%s-%s", *accessConfig.Region, *accessConfig.ClusterName)

err = files.CreateDir(exportPath)
if err != nil {
exit.Error(err)
}

for _, apiSpec := range apiSpecs {
baseDir := filepath.Join(exportPath, apiSpec.Name)

fmt.Println(fmt.Sprintf("exporting %s to %s", apiSpec.Name, baseDir))

err = files.CreateDir(baseDir)
if err != nil {
exit.Error(err)
}

err = awsClient.DownloadFileFromS3(info.ClusterConfig.Bucket, apiSpec.RawAPIKey(), path.Join(baseDir, apiSpec.FileName))
if err != nil {
exit.Error(err)
}

if apiSpec.Kind != userconfig.TrafficSplitterKind {
zipFileLocation := path.Join(baseDir, path.Base(apiSpec.ProjectKey))
err = awsClient.DownloadFileFromS3(info.ClusterConfig.Bucket, apiSpec.ProjectKey, zipFileLocation)
if err != nil {
exit.Error(err)
}

_, err = archive.UnzipFileToDir(zipFileLocation, baseDir)
if err != nil {
exit.Error(err)
}

err := os.Remove(zipFileLocation)
if err != nil {
exit.Error(err)
}
}
}
},
}

var _emailPrompValidation = &cr.PromptValidation{
PromptItemValidations: []*cr.PromptItemValidation{
{
Expand Down
5 changes: 2 additions & 3 deletions cli/cmd/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,10 @@ func ErrorInvalidOperatorEndpoint(endpoint string) error {
})
}

// err can be passed in as nil
func ErrorNoOperatorLoadBalancer(envName string) error {
func ErrorNoOperatorLoadBalancer() error {
return errors.WithStack(&errors.Error{
Kind: ErrNoOperatorLoadBalancer,
Message: fmt.Sprintf("unable to locate operator load balancer; you can attempt to resolve this issue and configure your CLI environment by running `cortex cluster info --env %s`", envName),
Message: "unable to locate operator load balancer",
})
}

Expand Down
1 change: 1 addition & 0 deletions dev/generate_cli_md.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ commands=(
"cluster up"
"cluster info"
"cluster configure"
"cluster export"
"cluster down"
"env configure"
"env list"
Expand Down
15 changes: 15 additions & 0 deletions docs/miscellaneous/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ Flags:
-h, --help help for configure
```

### cluster export

```text
download the code and configuration for all APIs deployed in a cluster

Usage:
cortex cluster export [flags]

Flags:
-c, --config string path to a cluster configuration file
--aws-key string aws access key id
--aws-secret string aws secret access key
-h, --help help for export
```

### cluster down

```text
Expand Down
8 changes: 8 additions & 0 deletions pkg/operator/resources/batchapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string) (*spec.API, string,
return nil, "", errors.Wrap(err, "upload api spec")
}

if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
return nil, "", errors.Wrap(err, "upload raw api spec")
}

err = applyK8sResources(api, prevVirtualService)
if err != nil {
go deleteK8sResources(api.Name)
Expand All @@ -75,6 +79,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string) (*spec.API, string,
return nil, "", errors.Wrap(err, "upload api spec")
}

if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
return nil, "", errors.Wrap(err, "upload raw api spec")
}

err = applyK8sResources(api, prevVirtualService)
if err != nil {
return nil, "", err
Expand Down
8 changes: 8 additions & 0 deletions pkg/operator/resources/realtimeapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string, force bool) (*spec.A
return nil, "", errors.Wrap(err, "upload api spec")
}

if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
return nil, "", errors.Wrap(err, "upload raw api spec")
}

// Use api spec indexed by PredictorID for replicas to prevent rolling updates when SpecID changes without PredictorID changing
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.PredictorKey); err != nil {
return nil, "", errors.Wrap(err, "upload predictor spec")
Expand Down Expand Up @@ -92,6 +96,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string, force bool) (*spec.A
return nil, "", errors.Wrap(err, "upload api spec")
}

if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
return nil, "", errors.Wrap(err, "upload raw api spec")
}

// Use api spec indexed by PredictorID for replicas to prevent rolling updates when SpecID changes without PredictorID changing
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.PredictorKey); err != nil {
return nil, "", errors.Wrap(err, "upload predictor spec")
Expand Down
12 changes: 12 additions & 0 deletions pkg/operator/resources/trafficsplitter/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ func UpdateAPI(apiConfig *userconfig.API, force bool) (*spec.API, string, error)
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.Key); err != nil {
return nil, "", errors.Wrap(err, "upload api spec")
}

if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
return nil, "", errors.Wrap(err, "upload raw api spec")
}

if err := applyK8sVirtualService(api, prevVirtualService); err != nil {
go deleteK8sResources(api.Name)
return nil, "", err
}

err = operator.AddAPIToAPIGateway(*api.Networking.Endpoint, api.Networking.APIGateway, false)
if err != nil {
go deleteK8sResources(api.Name)
Expand All @@ -58,9 +64,15 @@ func UpdateAPI(apiConfig *userconfig.API, force bool) (*spec.API, string, error)
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.Key); err != nil {
return nil, "", errors.Wrap(err, "upload api spec")
}

if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
return nil, "", errors.Wrap(err, "upload raw api spec")
}

if err := applyK8sVirtualService(api, prevVirtualService); err != nil {
return nil, "", err
}

if err := operator.UpdateAPIGatewayK8s(prevVirtualService, api, false); err != nil {
return nil, "", err
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/types/spec/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ func Key(apiName string, apiID string) string {
)
}

func (api API) RawAPIKey() string {
return filepath.Join(
"apis",
api.Name,
"raw_api",
api.ID,
consts.CortexVersion+"-cortex.yaml",
)
}

func MetadataRoot(apiName string) string {
return filepath.Join(
"apis",
Expand Down
8 changes: 7 additions & 1 deletion pkg/types/spec/validations.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/cortexlabs/cortex/pkg/types"
"github.com/cortexlabs/cortex/pkg/types/clusterconfig"
"github.com/cortexlabs/cortex/pkg/types/userconfig"
"github.com/cortexlabs/yaml"
kresource "k8s.io/apimachinery/pkg/api/resource"
)

Expand Down Expand Up @@ -643,10 +644,15 @@ func ExtractAPIConfigs(
return nil, errors.Append(err, fmt.Sprintf("\n\napi configuration schema for Traffic Splitter can be found at https://docs.cortex.dev/v/%s/deployments/realtime-api/traffic-splitter", consts.CortexVersionMinor))
}
}

api.Index = i
api.FileName = configFileName

rawYAMLBytes, err := yaml.Marshal([]map[string]interface{}{data})
if err != nil {
return nil, errors.Wrap(err, api.Identify())
}
api.RawYAMLBytes = rawYAMLBytes

if resourceStruct.Kind == userconfig.RealtimeAPIKind || resourceStruct.Kind == userconfig.BatchAPIKind {
api.ApplyDefaultDockerPaths()
}
Expand Down
1 change: 1 addition & 0 deletions pkg/types/userconfig/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type API struct {
UpdateStrategy *UpdateStrategy `json:"update_strategy" yaml:"update_strategy"`
Index int `json:"index" yaml:"-"`
FileName string `json:"file_name" yaml:"-"`
RawYAMLBytes []byte `json:"-" yaml:"-"`
}

type Predictor struct {
Expand Down