Skip to content

Commit c6a3894

Browse files
authored
Add support for exporting APIs (#1368)
1 parent ae7f2c0 commit c6a3894

File tree

11 files changed

+210
-5
lines changed

11 files changed

+210
-5
lines changed

cli/cluster/lib_http_client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ func (client *OperatorClient) MakeRequest(operatorConfig OperatorConfig, request
197197

198198
response, err := client.Do(request)
199199
if err != nil {
200+
if operatorConfig.EnvName == "" {
201+
return nil, errors.Wrap(err, "failed to connect to operator", operatorConfig.OperatorEndpoint)
202+
}
200203
return nil, ErrorFailedToConnectOperator(err, operatorConfig.EnvName, operatorConfig.OperatorEndpoint)
201204
}
202205
defer response.Body.Close()

cli/cmd/cluster.go

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cmd
1919
import (
2020
"fmt"
2121
"os"
22+
"path"
2223
"path/filepath"
2324
"strings"
2425
"time"
@@ -27,6 +28,7 @@ import (
2728
"github.com/cortexlabs/cortex/cli/cluster"
2829
"github.com/cortexlabs/cortex/cli/types/cliconfig"
2930
"github.com/cortexlabs/cortex/pkg/consts"
31+
"github.com/cortexlabs/cortex/pkg/lib/archive"
3032
"github.com/cortexlabs/cortex/pkg/lib/aws"
3133
cr "github.com/cortexlabs/cortex/pkg/lib/configreader"
3234
"github.com/cortexlabs/cortex/pkg/lib/console"
@@ -43,6 +45,8 @@ import (
4345
"github.com/cortexlabs/cortex/pkg/types"
4446
"github.com/cortexlabs/cortex/pkg/types/clusterconfig"
4547
"github.com/cortexlabs/cortex/pkg/types/clusterstate"
48+
"github.com/cortexlabs/cortex/pkg/types/spec"
49+
"github.com/cortexlabs/cortex/pkg/types/userconfig"
4650
"github.com/spf13/cobra"
4751
)
4852

@@ -89,6 +93,11 @@ func clusterInit() {
8993
addAWSCredentials(_downCmd)
9094
_downCmd.Flags().BoolVarP(&_flagClusterDisallowPrompt, "yes", "y", false, "skip prompts")
9195
_clusterCmd.AddCommand(_downCmd)
96+
97+
_exportCmd.Flags().SortFlags = false
98+
addClusterConfigFlag(_exportCmd)
99+
addAWSCredentials(_exportCmd)
100+
_clusterCmd.AddCommand(_exportCmd)
92101
}
93102

94103
func addClusterConfigFlag(cmd *cobra.Command) {
@@ -269,7 +278,7 @@ var _upCmd = &cobra.Command{
269278
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)))
270279
}
271280
if loadBalancer == nil {
272-
exit.Error(ErrorNoOperatorLoadBalancer(_flagClusterEnv))
281+
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)))
273282
}
274283

275284
newEnvironment := cliconfig.Environment{
@@ -434,6 +443,7 @@ var _downCmd = &cobra.Command{
434443
if err != nil {
435444
exit.Error(err)
436445
}
446+
437447
warnIfNotAdmin(awsClient)
438448

439449
clusterState, err := clusterstate.GetClusterState(awsClient, accessConfig)
@@ -533,6 +543,138 @@ var _downCmd = &cobra.Command{
533543
},
534544
}
535545

546+
var _exportCmd = &cobra.Command{
547+
Use: "export",
548+
Short: "download the code and configuration for all APIs deployed in a cluster",
549+
Args: cobra.NoArgs,
550+
Run: func(cmd *cobra.Command, args []string) {
551+
telemetry.Event("cli.cluster.export")
552+
553+
if _flagClusterConfig != "" {
554+
// Deprecation: specifying aws creds in cluster configuration is no longer supported
555+
if err := detectAWSCredsInConfigFile(cmd.Use, _flagClusterConfig); err != nil {
556+
exit.Error(err)
557+
}
558+
}
559+
560+
accessConfig, err := getClusterAccessConfig(_flagClusterDisallowPrompt)
561+
if err != nil {
562+
exit.Error(err)
563+
}
564+
565+
awsCreds, err := awsCredentialsForManagingCluster(*accessConfig, _flagClusterDisallowPrompt)
566+
if err != nil {
567+
exit.Error(err)
568+
}
569+
570+
// Check AWS access
571+
awsClient, err := newAWSClient(*accessConfig.Region, awsCreds)
572+
if err != nil {
573+
exit.Error(err)
574+
}
575+
warnIfNotAdmin(awsClient)
576+
577+
clusterState, err := clusterstate.GetClusterState(awsClient, accessConfig)
578+
if err != nil {
579+
exit.Error(err)
580+
}
581+
582+
err = clusterstate.AssertClusterStatus(*accessConfig.ClusterName, *accessConfig.Region, clusterState.Status, clusterstate.StatusCreateComplete)
583+
if err != nil {
584+
exit.Error(err)
585+
}
586+
587+
loadBalancer, err := awsClient.FindLoadBalancer(map[string]string{
588+
clusterconfig.ClusterNameTag: *accessConfig.ClusterName,
589+
"cortex.dev/load-balancer": "operator",
590+
})
591+
if err != nil {
592+
exit.Error(err)
593+
}
594+
if loadBalancer == nil {
595+
exit.Error(ErrorNoOperatorLoadBalancer())
596+
}
597+
598+
operatorConfig := cluster.OperatorConfig{
599+
Telemetry: isTelemetryEnabled(),
600+
ClientID: clientID(),
601+
AWSAccessKeyID: awsCreds.AWSAccessKeyID,
602+
AWSSecretAccessKey: awsCreds.AWSSecretAccessKey,
603+
OperatorEndpoint: "https://" + *loadBalancer.DNSName,
604+
}
605+
606+
info, err := cluster.Info(operatorConfig)
607+
if err != nil {
608+
exit.Error(err)
609+
}
610+
611+
apisResponse, err := cluster.GetAPIs(operatorConfig)
612+
if err != nil {
613+
exit.Error(err)
614+
}
615+
616+
var apiSpecs []spec.API
617+
618+
for _, batchAPI := range apisResponse.BatchAPIs {
619+
apiSpecs = append(apiSpecs, batchAPI.Spec)
620+
}
621+
622+
for _, realtimeAPI := range apisResponse.RealtimeAPIs {
623+
apiSpecs = append(apiSpecs, realtimeAPI.Spec)
624+
}
625+
626+
for _, trafficSplitter := range apisResponse.TrafficSplitters {
627+
apiSpecs = append(apiSpecs, trafficSplitter.Spec)
628+
}
629+
630+
if len(apiSpecs) == 0 {
631+
fmt.Println(fmt.Sprintf("no apis found in cluster named %s in %s", *accessConfig.ClusterName, *accessConfig.Region))
632+
exit.Ok()
633+
}
634+
635+
exportPath := fmt.Sprintf("export-%s-%s", *accessConfig.Region, *accessConfig.ClusterName)
636+
637+
err = files.CreateDir(exportPath)
638+
if err != nil {
639+
exit.Error(err)
640+
}
641+
642+
for _, apiSpec := range apiSpecs {
643+
baseDir := filepath.Join(exportPath, apiSpec.Name)
644+
645+
fmt.Println(fmt.Sprintf("exporting %s to %s", apiSpec.Name, baseDir))
646+
647+
err = files.CreateDir(baseDir)
648+
if err != nil {
649+
exit.Error(err)
650+
}
651+
652+
err = awsClient.DownloadFileFromS3(info.ClusterConfig.Bucket, apiSpec.RawAPIKey(), path.Join(baseDir, apiSpec.FileName))
653+
if err != nil {
654+
exit.Error(err)
655+
}
656+
657+
if apiSpec.Kind != userconfig.TrafficSplitterKind {
658+
zipFileLocation := path.Join(baseDir, path.Base(apiSpec.ProjectKey))
659+
err = awsClient.DownloadFileFromS3(info.ClusterConfig.Bucket, apiSpec.ProjectKey, zipFileLocation)
660+
if err != nil {
661+
exit.Error(err)
662+
}
663+
664+
_, err = archive.UnzipFileToDir(zipFileLocation, baseDir)
665+
if err != nil {
666+
exit.Error(err)
667+
}
668+
669+
err := os.Remove(zipFileLocation)
670+
if err != nil {
671+
exit.Error(err)
672+
}
673+
}
674+
}
675+
},
676+
}
677+
536678
var _emailPrompValidation = &cr.PromptValidation{
537679
PromptItemValidations: []*cr.PromptItemValidation{
538680
{

cli/cmd/errors.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,10 @@ func ErrorInvalidOperatorEndpoint(endpoint string) error {
133133
})
134134
}
135135

136-
// err can be passed in as nil
137-
func ErrorNoOperatorLoadBalancer(envName string) error {
136+
func ErrorNoOperatorLoadBalancer() error {
138137
return errors.WithStack(&errors.Error{
139138
Kind: ErrNoOperatorLoadBalancer,
140-
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),
139+
Message: "unable to locate operator load balancer",
141140
})
142141
}
143142

dev/generate_cli_md.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ commands=(
3737
"cluster up"
3838
"cluster info"
3939
"cluster configure"
40+
"cluster export"
4041
"cluster down"
4142
"env configure"
4243
"env list"

docs/miscellaneous/cli.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,21 @@ Flags:
156156
-h, --help help for configure
157157
```
158158

159+
### cluster export
160+
161+
```text
162+
download the code and configuration for all APIs deployed in a cluster
163+
164+
Usage:
165+
cortex cluster export [flags]
166+
167+
Flags:
168+
-c, --config string path to a cluster configuration file
169+
--aws-key string aws access key id
170+
--aws-secret string aws secret access key
171+
-h, --help help for export
172+
```
173+
159174
### cluster down
160175

161176
```text

pkg/operator/resources/batchapi/api.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string) (*spec.API, string,
4949
return nil, "", errors.Wrap(err, "upload api spec")
5050
}
5151

52+
if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
53+
return nil, "", errors.Wrap(err, "upload raw api spec")
54+
}
55+
5256
err = applyK8sResources(api, prevVirtualService)
5357
if err != nil {
5458
go deleteK8sResources(api.Name)
@@ -75,6 +79,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string) (*spec.API, string,
7579
return nil, "", errors.Wrap(err, "upload api spec")
7680
}
7781

82+
if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
83+
return nil, "", errors.Wrap(err, "upload raw api spec")
84+
}
85+
7886
err = applyK8sResources(api, prevVirtualService)
7987
if err != nil {
8088
return nil, "", err

pkg/operator/resources/realtimeapi/api.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string, force bool) (*spec.A
5959
return nil, "", errors.Wrap(err, "upload api spec")
6060
}
6161

62+
if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
63+
return nil, "", errors.Wrap(err, "upload raw api spec")
64+
}
65+
6266
// Use api spec indexed by PredictorID for replicas to prevent rolling updates when SpecID changes without PredictorID changing
6367
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.PredictorKey); err != nil {
6468
return nil, "", errors.Wrap(err, "upload predictor spec")
@@ -92,6 +96,10 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string, force bool) (*spec.A
9296
return nil, "", errors.Wrap(err, "upload api spec")
9397
}
9498

99+
if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
100+
return nil, "", errors.Wrap(err, "upload raw api spec")
101+
}
102+
95103
// Use api spec indexed by PredictorID for replicas to prevent rolling updates when SpecID changes without PredictorID changing
96104
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.PredictorKey); err != nil {
97105
return nil, "", errors.Wrap(err, "upload predictor spec")

pkg/operator/resources/trafficsplitter/api.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,16 @@ func UpdateAPI(apiConfig *userconfig.API, force bool) (*spec.API, string, error)
4242
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.Key); err != nil {
4343
return nil, "", errors.Wrap(err, "upload api spec")
4444
}
45+
46+
if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
47+
return nil, "", errors.Wrap(err, "upload raw api spec")
48+
}
49+
4550
if err := applyK8sVirtualService(api, prevVirtualService); err != nil {
4651
go deleteK8sResources(api.Name)
4752
return nil, "", err
4853
}
54+
4955
err = operator.AddAPIToAPIGateway(*api.Networking.Endpoint, api.Networking.APIGateway, false)
5056
if err != nil {
5157
go deleteK8sResources(api.Name)
@@ -58,9 +64,15 @@ func UpdateAPI(apiConfig *userconfig.API, force bool) (*spec.API, string, error)
5864
if err := config.AWS.UploadJSONToS3(api, config.Cluster.Bucket, api.Key); err != nil {
5965
return nil, "", errors.Wrap(err, "upload api spec")
6066
}
67+
68+
if err := config.AWS.UploadBytesToS3(api.RawYAMLBytes, config.Cluster.Bucket, api.RawAPIKey()); err != nil {
69+
return nil, "", errors.Wrap(err, "upload raw api spec")
70+
}
71+
6172
if err := applyK8sVirtualService(api, prevVirtualService); err != nil {
6273
return nil, "", err
6374
}
75+
6476
if err := operator.UpdateAPIGatewayK8s(prevVirtualService, api, false); err != nil {
6577
return nil, "", err
6678
}

pkg/types/spec/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ func Key(apiName string, apiID string) string {
154154
)
155155
}
156156

157+
func (api API) RawAPIKey() string {
158+
return filepath.Join(
159+
"apis",
160+
api.Name,
161+
"raw_api",
162+
api.ID,
163+
consts.CortexVersion+"-cortex.yaml",
164+
)
165+
}
166+
157167
func MetadataRoot(apiName string) string {
158168
return filepath.Join(
159169
"apis",

pkg/types/spec/validations.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/cortexlabs/cortex/pkg/types"
4444
"github.com/cortexlabs/cortex/pkg/types/clusterconfig"
4545
"github.com/cortexlabs/cortex/pkg/types/userconfig"
46+
"github.com/cortexlabs/yaml"
4647
kresource "k8s.io/apimachinery/pkg/api/resource"
4748
)
4849

@@ -643,10 +644,15 @@ func ExtractAPIConfigs(
643644
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))
644645
}
645646
}
646-
647647
api.Index = i
648648
api.FileName = configFileName
649649

650+
rawYAMLBytes, err := yaml.Marshal([]map[string]interface{}{data})
651+
if err != nil {
652+
return nil, errors.Wrap(err, api.Identify())
653+
}
654+
api.RawYAMLBytes = rawYAMLBytes
655+
650656
if resourceStruct.Kind == userconfig.RealtimeAPIKind || resourceStruct.Kind == userconfig.BatchAPIKind {
651657
api.ApplyDefaultDockerPaths()
652658
}

0 commit comments

Comments
 (0)