Skip to content

Commit e7588f0

Browse files
authored
deployment: New extension create subcommand (#470)
Adds support for create and upload actions for extensions - create <extension id> --version <version> --type <extension type> {--file <file-path> | --download-url <url>} [--description <description>]: Creates an extension.
1 parent 968729d commit e7588f0

8 files changed

+376
-0
lines changed

cmd/deployment/extension/create.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package cmddeploymentextension
19+
20+
import (
21+
"errors"
22+
"os"
23+
24+
"github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/extensionapi"
25+
"github.com/spf13/cobra"
26+
27+
"github.com/elastic/ecctl/pkg/ecctl"
28+
)
29+
30+
var createCmd = &cobra.Command{
31+
Use: "create <extension name> --version <version> --type <extension type> {--file <file-path> | --download-url <url>} [--description <description>]",
32+
Short: "Creates an extension",
33+
PreRunE: cobra.ExactArgs(1),
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
version, _ := cmd.Flags().GetString("version")
36+
extType, _ := cmd.Flags().GetString("type")
37+
url, _ := cmd.Flags().GetString("download-url")
38+
description, _ := cmd.Flags().GetString("description")
39+
file, _ := cmd.Flags().GetString("file")
40+
41+
if url != "" && file != "" {
42+
return errors.New("both --file and --download-url are set. Only one may be used")
43+
}
44+
45+
res, err := extensionapi.Create(extensionapi.CreateParams{
46+
API: ecctl.Get().API,
47+
Name: args[0],
48+
Version: version,
49+
Type: extType,
50+
DownloadURL: url,
51+
Description: description,
52+
})
53+
if err != nil {
54+
return err
55+
}
56+
57+
if file != "" {
58+
f, err := os.Open(file)
59+
if err != nil {
60+
return err
61+
}
62+
defer f.Close()
63+
64+
res2, err := extensionapi.Upload(extensionapi.UploadParams{
65+
API: ecctl.Get().API,
66+
ExtensionID: *res.ID,
67+
File: f,
68+
})
69+
if err != nil {
70+
return err
71+
}
72+
73+
return ecctl.Get().Formatter.Format("", res2)
74+
}
75+
76+
return ecctl.Get().Formatter.Format("", res)
77+
},
78+
}
79+
80+
func init() {
81+
initCreateFlags()
82+
}
83+
84+
func initCreateFlags() {
85+
Command.AddCommand(createCmd)
86+
createCmd.Flags().String("version", "", "Elastic stack version. Numeric version for plugins, e.g. 7.10.0. Major version e.g. 7.*, or wildcards e.g. * for bundles.")
87+
createCmd.Flags().String("type", "", "Extension type. Can be one of [bundle, plugin].")
88+
createCmd.Flags().String("download-url", "", "Optional flag to define the URL to download the extension archive.")
89+
createCmd.Flags().String("description", "", "Optional flag to add a description to the extension.")
90+
createCmd.Flags().String("file", "", "Optional flag to upload an extension from a local file path.")
91+
cobra.MarkFlagRequired(createCmd.Flags(), "version")
92+
cobra.MarkFlagRequired(createCmd.Flags(), "type")
93+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package cmddeploymentextension
19+
20+
import (
21+
"encoding/json"
22+
"io/ioutil"
23+
"testing"
24+
25+
"github.com/elastic/cloud-sdk-go/pkg/api"
26+
"github.com/elastic/cloud-sdk-go/pkg/api/mock"
27+
"github.com/elastic/cloud-sdk-go/pkg/models"
28+
29+
"github.com/elastic/ecctl/cmd/util/testutils"
30+
)
31+
32+
func Test_createCmd(t *testing.T) {
33+
createRawResp, err := ioutil.ReadFile("./testdata/create.json")
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
var succeedResp = new(models.Extension)
39+
if err := succeedResp.UnmarshalBinary(createRawResp); err != nil {
40+
t.Fatal(err)
41+
}
42+
43+
createJSONOutput, err := json.MarshalIndent(succeedResp, "", " ")
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
48+
tests := []struct {
49+
name string
50+
args testutils.Args
51+
want testutils.Assertion
52+
}{
53+
{
54+
name: "fails due to empty argument",
55+
args: testutils.Args{
56+
Cmd: createCmd,
57+
Args: []string{
58+
"create",
59+
},
60+
Cfg: testutils.MockCfg{Responses: []mock.Response{
61+
mock.SampleInternalError(),
62+
}},
63+
},
64+
want: testutils.Assertion{
65+
Err: `accepts 1 arg(s), received 0`,
66+
},
67+
},
68+
{
69+
name: "fails due to missing flags",
70+
args: testutils.Args{
71+
Cmd: createCmd,
72+
Args: []string{
73+
"create", "mybundle",
74+
},
75+
Cfg: testutils.MockCfg{Responses: []mock.Response{
76+
mock.SampleInternalError(),
77+
}},
78+
},
79+
want: testutils.Assertion{
80+
Err: `required flag(s) "type", "version" not set`,
81+
},
82+
},
83+
{
84+
name: "fails due to mismatching flags",
85+
args: testutils.Args{
86+
Cmd: createCmd,
87+
Args: []string{
88+
"create", "mybundle", "--version", "*", "--type", "bundle",
89+
"--download-url", "example.com", "--file", "hi.zip",
90+
},
91+
Cfg: testutils.MockCfg{Responses: []mock.Response{
92+
mock.SampleInternalError(),
93+
}},
94+
},
95+
want: testutils.Assertion{
96+
Err: "both --file and --download-url are set. Only one may be used",
97+
},
98+
},
99+
{
100+
name: "fails due to API error",
101+
args: testutils.Args{
102+
Cmd: createCmd,
103+
Args: []string{
104+
"create", "mybundle", "--version", "*", "--type", "bundle",
105+
},
106+
Cfg: testutils.MockCfg{Responses: []mock.Response{
107+
mock.SampleInternalError(),
108+
}},
109+
},
110+
want: testutils.Assertion{
111+
Err: mock.MultierrorInternalError.Error(),
112+
},
113+
},
114+
{
115+
name: "succeeds",
116+
args: testutils.Args{
117+
Cmd: createCmd,
118+
Args: []string{
119+
"create", "mybundle", "--version", "*", "--type", "bundle", "--description", "Why hello there",
120+
},
121+
Cfg: testutils.MockCfg{
122+
OutputFormat: "json",
123+
Responses: []mock.Response{
124+
mock.New200ResponseAssertion(
125+
&mock.RequestAssertion{
126+
Header: api.DefaultWriteMockHeaders,
127+
Method: "POST",
128+
Path: "/api/v1/deployments/extensions",
129+
Host: api.DefaultMockHost,
130+
Body: mock.NewStringBody(`{"description":"Why hello there","extension_type":"bundle","name":"mybundle","version":"*"}` + "\n"),
131+
},
132+
mock.NewByteBody(createRawResp),
133+
),
134+
},
135+
},
136+
},
137+
want: testutils.Assertion{
138+
Err: string(createJSONOutput) + "\n",
139+
},
140+
},
141+
{
142+
name: "succeeds with upload",
143+
args: testutils.Args{
144+
Cmd: createCmd,
145+
Args: []string{
146+
"create", "mybundle", "--version", "*", "--type", "bundle", "--file",
147+
"./testdata/extension.zip", "--description", "Why hello there",
148+
},
149+
Cfg: testutils.MockCfg{
150+
OutputFormat: "json",
151+
Responses: []mock.Response{
152+
mock.New200Response(
153+
mock.NewByteBody(createRawResp),
154+
),
155+
mock.New200Response(
156+
mock.NewByteBody(createRawResp),
157+
),
158+
},
159+
},
160+
},
161+
want: testutils.Assertion{
162+
Err: string(createJSONOutput) + "\n",
163+
},
164+
},
165+
}
166+
for _, tt := range tests {
167+
t.Run(tt.name, func(t *testing.T) {
168+
testutils.RunCmdAssertion(t, tt.args, tt.want)
169+
tt.args.Cmd.ResetFlags()
170+
defer initCreateFlags()
171+
})
172+
}
173+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"deployments": null,
3+
"description": "Why hello there",
4+
"extension_type": "bundle",
5+
"file_metadata": {
6+
"last_modified_date": "0001-01-01T00:00:00.000Z"
7+
},
8+
"id": "4237175152",
9+
"name": "mybundle",
10+
"url": "repo://4237175152",
11+
"version": "*"
12+
}

docs/ecctl-command-reference-index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ include::ecctl_deployment_elasticsearch_keystore.adoc[]
1919
include::ecctl_deployment_elasticsearch_keystore_show.adoc[]
2020
include::ecctl_deployment_elasticsearch_keystore_update.adoc[]
2121
include::ecctl_deployment_extension.adoc[]
22+
include::ecctl_deployment_extension_create.adoc[]
2223
include::ecctl_deployment_extension_delete.adoc[]
2324
include::ecctl_deployment_extension_list.adoc[]
2425
include::ecctl_deployment_extension_show.adoc[]

docs/ecctl_deployment_extension.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ ecctl deployment extension [flags]
4242
=== SEE ALSO
4343

4444
* xref:ecctl_deployment[ecctl deployment] - Manages deployments
45+
* xref:ecctl_deployment_extension_create[ecctl deployment extension create] - Creates an extension
4546
* xref:ecctl_deployment_extension_delete[ecctl deployment extension delete] - Deletes a deployment extension
4647
* xref:ecctl_deployment_extension_list[ecctl deployment extension list] - Lists all deployment extensions
4748
* xref:ecctl_deployment_extension_show[ecctl deployment extension show] - Shows information about a deployment extension

docs/ecctl_deployment_extension.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ecctl deployment extension [flags]
3838
### SEE ALSO
3939

4040
* [ecctl deployment](ecctl_deployment.md) - Manages deployments
41+
* [ecctl deployment extension create](ecctl_deployment_extension_create.md) - Creates an extension
4142
* [ecctl deployment extension delete](ecctl_deployment_extension_delete.md) - Deletes a deployment extension
4243
* [ecctl deployment extension list](ecctl_deployment_extension_list.md) - Lists all deployment extensions
4344
* [ecctl deployment extension show](ecctl_deployment_extension_show.md) - Shows information about a deployment extension
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[#ecctl_deployment_extension_create]
2+
== ecctl deployment extension create
3+
4+
Creates an extension
5+
6+
----
7+
ecctl deployment extension create <extension name> --version <version> --type <extension type> {--file <file-path> | --download-url <url>} [--description <description>] [flags]
8+
----
9+
10+
[float]
11+
=== Options
12+
13+
----
14+
--description string Optional flag to add a description to the extension.
15+
--download-url string Optional flag to define the URL to download the extension archive.
16+
--file string Optional flag to upload an extension from a local file path.
17+
-h, --help help for create
18+
--type string Extension type. Can be one of [bundle, plugin].
19+
--version string Elastic stack version. Numeric version for plugins, e.g. 7.10.0. Major version e.g. 7.*, or wildcards e.g. * for bundles.
20+
----
21+
22+
[float]
23+
=== Options inherited from parent commands
24+
25+
----
26+
--api-key string API key to use to authenticate (If empty will look for EC_API_KEY environment variable)
27+
--config string Config name, used to have multiple configs in $HOME/.ecctl/<env> (default "config")
28+
--force Do not ask for confirmation
29+
--format string Formats the output using a Go template
30+
--host string Base URL to use
31+
--insecure Skips all TLS validation
32+
--message string A message to set on cluster operation
33+
--output string Output format [text|json] (default "text")
34+
--pass string Password to use to authenticate (If empty will look for EC_PASS environment variable)
35+
--pprof Enables pprofing and saves the profile to pprof-20060102150405
36+
-q, --quiet Suppresses the configuration file used for the run, if any
37+
--region string Elasticsearch Service region
38+
--timeout duration Timeout to use on all HTTP calls (default 30s)
39+
--trace Enables tracing saves the trace to trace-20060102150405
40+
--user string Username to use to authenticate (If empty will look for EC_USER environment variable)
41+
--verbose Enable verbose mode
42+
--verbose-credentials When set, Authorization headers on the request/response trail will be displayed as plain text
43+
--verbose-file string When set, the verbose request/response trail will be written to the defined file
44+
----
45+
46+
[float]
47+
=== SEE ALSO
48+
49+
* xref:ecctl_deployment_extension[ecctl deployment extension] - Manages deployment extensions, such as custom plugins or bundles

0 commit comments

Comments
 (0)