Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ jobs:
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: 'go.mod'
- name: 🧹 Frieza
uses: outscale/frieza-github-actions/frieza-clean@master
with:
access_key: ${{ secrets.OSC_ACCESS_KEY }}
secret_key: ${{ secrets.OSC_SECRET_KEY }}
region: "eu-west-2"
- name: 🧪 Test
run: go test ./...
env:
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,22 +138,38 @@ gli <command> <api call>

## 💡 Examples

### ReadVms
### Querying oapi

List all VMs in the `running` state:
```bash
```shell
gli oapi ReadVms --Filters.VmStateNames running
```

The flag syntax is:
* list of values are comma-separated: `--Filters.VmStateNames running,stopped`
* boolean flags can be set to false by setting: `--TmpEnabled=false`

### Multiple values

Lists of embedded objects (e.g. `Nics` or `BlockDeviceMappings` in `CreateVms`) can be configured using indexes: `--BlockDeviceMappings.0.Bsu.VolumeType`.

### Chaining

Commands may be chained, and attributes returned by a command can be reinjected in a subsequent command, using Go template syntax:

```shell
gli oapi CreateNic --SubnetId subnet-foo | gli oapi LinkNic -v --NicId {{.Nic.NicId}} --VmId i-foo --DeviceNumber 7
```

### Using jq filters

```bash
```shell
gli oapi ReadVms --Filters.VmStateNames running --jq ".Vms[].VmId"
```

### Self updating

```bash
```shell
gli update
```

Expand Down
49 changes: 49 additions & 0 deletions cmd/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd_test

import (
"encoding/json"
"io"
"os"
"path/filepath"
"testing"

"github.com/outscale/gli/cmd"
"github.com/outscale/gli/pkg/runner"
"github.com/stretchr/testify/require"
)

func run(t *testing.T, args []string, input []byte) []byte {
os.Args = append([]string{"gli"}, args...)
stdin, stdout := os.Stdin, os.Stdout
defer func() {
os.Stdin, os.Stdout = stdin, stdout
}()
dir := t.TempDir()
var err error
if len(input) > 0 {
os.Stdin, err = os.Create(filepath.Join(dir, "stdin")) //nolint
require.NoError(t, err)
_, err = os.Stdin.Write(input)
require.NoError(t, err)
_, err = os.Stdin.Seek(0, io.SeekStart)
require.NoError(t, err)
}
os.Stdout, err = os.Create(filepath.Join(dir, "stdout")) //nolint
require.NoError(t, err)

err = runner.Prefilter()
require.NoError(t, err)
cmd.Execute()

err = os.Stdout.Close()
require.NoError(t, err)
content, err := os.ReadFile(os.Stdout.Name())
require.NoError(t, err)
return content
}

func runJSON(t *testing.T, args []string, input []byte, resp any) {
content := run(t, args, input)
err := json.Unmarshal(content, &resp)
require.NoError(t, err)
}
8 changes: 4 additions & 4 deletions cmd/oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import (
"reflect"
"strings"

"github.com/outscale/gli/pkg/builder"
"github.com/outscale/gli/pkg/debug"
"github.com/outscale/gli/pkg/errors"
"github.com/outscale/gli/pkg/flags"
"github.com/outscale/gli/pkg/openapi"
"github.com/outscale/gli/pkg/output"
"github.com/outscale/gli/pkg/runner"
"github.com/outscale/gli/pkg/sdk"
"github.com/outscale/gli/pkg/version"
"github.com/outscale/osc-sdk-go/v3/pkg/middleware"
Expand All @@ -36,13 +37,12 @@ var oapiCmd = &cobra.Command{

func init() {
rootCmd.AddCommand(oapiCmd)

ospec, err := osc.GetSwagger()
if err != nil {
errors.Warn(fmt.Sprintf("⚠️ unable to load OpenAPI spec: %v", err))
}
spec := openapi.NewSpec(ospec)
b := flags.NewBuilder(spec)
b := builder.NewBuilder(spec)

c := reflect.TypeOf(&osc.Client{})
for i := range c.NumMethod() {
Expand Down Expand Up @@ -91,7 +91,7 @@ func oapi(cmd *cobra.Command) {
m, _ := clt.MethodByName(cmd.Name())
argType := m.Type.In(2)
arg := reflect.New(argType)
err = flags.ToStruct(cmd, arg, "")
err = runner.ToStruct(cmd, arg, "")
if err != nil {
errors.ExitErr(err)
}
Expand Down
30 changes: 10 additions & 20 deletions cmd/oapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,32 @@ SPDX-License-Identifier: BSD-3-Clause
package cmd_test

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/outscale/gli/cmd"
"github.com/outscale/osc-sdk-go/v3/pkg/osc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestOAPI(t *testing.T) {
t.Run("ReadVms works", func(t *testing.T) {
os.Args = []string{"gli", "oapi", "ReadVms", "-v", "--Filters.VmStateNames", "running"}
stdout := os.Stdout
defer func() {
os.Stdout = stdout
}()
dir := t.TempDir()
var err error
os.Stdout, err = os.Create(filepath.Join(dir, "stdout")) //nolint
require.NoError(t, err)
cmd.Execute()

err = os.Stdout.Close()
require.NoError(t, err)
content, err := os.ReadFile(os.Stdout.Name())
require.NoError(t, err)
resp := osc.ReadVmsResponse{}
err = json.Unmarshal(content, &resp)
require.NoError(t, err)
runJSON(t, []string{"oapi", "ReadVms", "-v", "--Filters.VmStateNames", "running"}, nil, &resp)
require.NotNil(t, resp.Vms)
assert.NotEmpty(t, *resp.Vms)
for _, vm := range *resp.Vms {
assert.Equal(t, osc.VmStateRunning, vm.State)
}
require.NotNil(t, resp.ResponseContext)
assert.NotEmpty(t, resp.ResponseContext.RequestId)
})
t.Run("Chaining works", func(t *testing.T) {
region := os.Getenv("OSC_REGION")
out := run(t, []string{"oapi", "CreateNet", "--IpRange", "10.0.0.0/16"}, nil)
resp := osc.CreateSubnetResponse{}
runJSON(t, []string{"oapi", "CreateSubnet", "--NetId", "{{.Net.NetId}}", "--IpRange", "10.0.1.0/24", "--SubregionName", region + "a"}, out, &resp)
require.NotNil(t, resp.Subnet)
assert.NotEmpty(t, resp.Subnet.SubnetId)
})
}
10 changes: 9 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ SPDX-License-Identifier: BSD-3-Clause
*/
package main

import "github.com/outscale/gli/cmd"
import (
"github.com/outscale/gli/cmd"
"github.com/outscale/gli/pkg/errors"
"github.com/outscale/gli/pkg/runner"
)

func main() {
err := runner.Prefilter()
if err != nil {
errors.ExitErr(err)
}
cmd.Execute()
}
81 changes: 81 additions & 0 deletions pkg/builder/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
SPDX-FileCopyrightText: 2026 Outscale SAS <opensource@outscale.com>

SPDX-License-Identifier: BSD-3-Clause
*/package builder

import (
"encoding/json"
"reflect"
"strconv"

"github.com/outscale/gli/pkg/openapi"
"github.com/outscale/gli/pkg/options"
"github.com/samber/lo"
"github.com/spf13/cobra"
)

type Builder struct {
spec *openapi.Spec
}

func NewBuilder(spec *openapi.Spec) *Builder {
return &Builder{spec: spec}
}

func (b *Builder) FromStruct(cmd *cobra.Command, arg reflect.Type, prefix string) {
typeName := arg.Name()
fs := cmd.Flags()
for i := range arg.NumField() {
f := arg.Field(i)
t := f.Type
ot := t
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
help := b.spec.SummaryForAttribute(typeName, f.Name)
switch t.Kind() {
case reflect.Bool:
fs.Bool(prefix+f.Name, false, help)
case reflect.String:
fs.String(prefix+f.Name, "", help)
if t.Implements(reflect.TypeFor[enum]()) {
values := reflect.New(t).Interface().(enum).Values()
_ = cmd.RegisterFlagCompletionFunc(prefix+f.Name, func(_ *cobra.Command, _ []string, _ string) ([]cobra.Completion, cobra.ShellCompDirective) {
return lo.Map(values, func(v string, _ int) cobra.Completion { return cobra.Completion(v) }), cobra.ShellCompDirectiveDefault
})
}
case reflect.Int:
fs.Int(prefix+f.Name, 0, help)
case reflect.Slice:
switch t.Elem().Kind() {
case reflect.Bool:
fs.BoolSlice(prefix+f.Name, nil, help)
case reflect.String:
fs.StringSlice(prefix+f.Name, nil, help)
if t.Elem().Implements(reflect.TypeFor[enum]()) {
values := reflect.New(t.Elem()).Interface().(enum).Values()
_ = cmd.RegisterFlagCompletionFunc(prefix+f.Name, func(_ *cobra.Command, _ []string, _ string) ([]cobra.Completion, cobra.ShellCompDirective) {
return lo.Map(values, func(v string, _ int) cobra.Completion { return cobra.Completion(v) }), cobra.ShellCompDirectiveDefault
})
}
case reflect.Int:
fs.IntSlice(prefix+f.Name, nil, help)
case reflect.Struct:
if t.Elem().Implements(reflect.TypeFor[json.Marshaler]()) {
fs.StringSlice(prefix+f.Name, nil, help)
} else {
for i := range options.NumEntriesInSlices {
b.FromStruct(cmd, t.Elem(), prefix+f.Name+"."+strconv.Itoa(i)+".")
}
}
}
case reflect.Struct:
if ot.Implements(reflect.TypeFor[json.Marshaler]()) {
fs.String(prefix+f.Name, "", help)
} else {
b.FromStruct(cmd, t, prefix+f.Name+".")
}
}
}
}
2 changes: 1 addition & 1 deletion pkg/flags/enum.go → pkg/builder/enum.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package flags
package builder

type enum interface {
Values() []string
Expand Down
20 changes: 20 additions & 0 deletions pkg/options/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2026 Outscale SAS <opensource@outscale.com>

SPDX-License-Identifier: BSD-3-Clause
*/
package options

import (
"os"
"strconv"
)

var NumEntriesInSlices = 1

func init() {
num := os.Getenv("NUM_ENTRIES")
if num != "" {
NumEntriesInSlices, _ = strconv.Atoi(num)
}
}
Loading