github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/objects/app/apps.go (about)

     1  /*
     2   * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package app
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"text/tabwriter"
    25  
    26  	"context"
    27  	"strings"
    28  
    29  	"github.com/fnproject/cli/client"
    30  	"github.com/fnproject/cli/common"
    31  
    32  	fnclient "github.com/fnproject/fn_go/clientv2"
    33  	apiapps "github.com/fnproject/fn_go/clientv2/apps"
    34  	"github.com/fnproject/fn_go/modelsv2"
    35  	"github.com/fnproject/fn_go/provider"
    36  	"github.com/jmoiron/jsonq"
    37  	"github.com/urfave/cli"
    38  )
    39  
    40  const (
    41  	SHAPE_PARAMETER = "shape"
    42  )
    43  
    44  type appsCmd struct {
    45  	provider provider.Provider
    46  	client   *fnclient.Fn
    47  }
    48  
    49  func printApps(c *cli.Context, apps []*modelsv2.App) error {
    50  	outputFormat := strings.ToLower(c.String("output"))
    51  	if outputFormat == "json" {
    52  		var allApps []interface{}
    53  		for _, app := range apps {
    54  			a := struct {
    55  				Name string `json:"name"`
    56  				ID   string `json:"id"`
    57  			}{app.Name,
    58  				app.ID,
    59  			}
    60  			allApps = append(allApps, a)
    61  		}
    62  		b, err := json.MarshalIndent(allApps, "", "    ")
    63  		if err != nil {
    64  			return err
    65  		}
    66  		fmt.Fprint(os.Stdout, string(b))
    67  	} else {
    68  		w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
    69  		fmt.Fprint(w, "NAME", "\t", "ID", "\t", "\n")
    70  		for _, app := range apps {
    71  			fmt.Fprint(w, app.Name, "\t", app.ID, "\t", "\n")
    72  
    73  		}
    74  		if err := w.Flush(); err != nil {
    75  			return err
    76  		}
    77  	}
    78  	return nil
    79  }
    80  
    81  func (a *appsCmd) list(c *cli.Context) error {
    82  	resApps, err := getApps(c, a.client)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	return printApps(c, resApps)
    87  }
    88  
    89  // getApps returns an array of all apps in the given context and client
    90  func getApps(c *cli.Context, client *fnclient.Fn) ([]*modelsv2.App, error) {
    91  	params := &apiapps.ListAppsParams{Context: context.Background()}
    92  	var resApps []*modelsv2.App
    93  	for {
    94  		resp, err := client.Apps.ListApps(params)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  
    99  		resApps = append(resApps, resp.Payload.Items...)
   100  
   101  		n := c.Int64("n")
   102  
   103  		howManyMore := n - int64(len(resApps)+len(resp.Payload.Items))
   104  		if howManyMore <= 0 || resp.Payload.NextCursor == "" {
   105  			break
   106  		}
   107  
   108  		params.Cursor = &resp.Payload.NextCursor
   109  	}
   110  
   111  	if len(resApps) == 0 {
   112  		fmt.Fprint(os.Stderr, "No apps found\n")
   113  		return nil, nil
   114  	}
   115  	return resApps, nil
   116  }
   117  
   118  // BashCompleteApps can be called from a BashComplete function
   119  // to provide app completion suggestions (Does not check if the
   120  // current context already contains an app name as an argument.
   121  // This should be checked before calling this)
   122  func BashCompleteApps(c *cli.Context) {
   123  	provider, err := client.CurrentProvider()
   124  	if err != nil {
   125  		return
   126  	}
   127  	resp, err := getApps(c, provider.APIClientv2())
   128  	if err != nil {
   129  		return
   130  	}
   131  	for _, r := range resp {
   132  		fmt.Println(r.Name)
   133  	}
   134  }
   135  
   136  func appWithFlags(c *cli.Context, app *modelsv2.App) {
   137  	if c.IsSet("syslog-url") {
   138  		str := c.String("syslog-url")
   139  		app.SyslogURL = &str
   140  	}
   141  	if len(c.StringSlice("config")) > 0 {
   142  		app.Config = common.ExtractConfig(c.StringSlice("config"))
   143  	}
   144  	if len(c.StringSlice("annotation")) > 0 {
   145  		app.Annotations = common.ExtractAnnotations(c)
   146  	}
   147  }
   148  
   149  func (a *appsCmd) create(c *cli.Context) error {
   150  	app := &modelsv2.App{
   151  		Name: c.Args().Get(0),
   152  	}
   153  
   154  	appWithFlags(c, app)
   155  	// If architectures flag is not set then default it to nil
   156  	if c.IsSet(SHAPE_PARAMETER) {
   157  		shapeParam := c.String(SHAPE_PARAMETER)
   158  
   159  		// Check for architectures parameter passed or set to default
   160  		if len(shapeParam) == 0 {
   161  			return errors.New("no shape specified for the application")
   162  		}
   163  
   164  		if _, ok := common.ShapeMap[shapeParam]; !ok {
   165  			return errors.New("invalid shape specified for the application")
   166  		}
   167  		app.Shape = shapeParam
   168  	}
   169  	_, err := CreateApp(a.client, app)
   170  	return err
   171  }
   172  
   173  // CreateApp creates a new app using the given client
   174  func CreateApp(a *fnclient.Fn, app *modelsv2.App) (*modelsv2.App, error) {
   175  	resp, err := a.Apps.CreateApp(&apiapps.CreateAppParams{
   176  		Context: context.Background(),
   177  		Body:    app,
   178  	})
   179  
   180  	if err != nil {
   181  		switch e := err.(type) {
   182  		case *apiapps.CreateAppBadRequest:
   183  			err = fmt.Errorf("%v", e.Payload.Message)
   184  		case *apiapps.CreateAppConflict:
   185  			err = fmt.Errorf("%v", e.Payload.Message)
   186  		}
   187  		return nil, err
   188  	}
   189  	fmt.Println("Successfully created app: ", resp.Payload.Name)
   190  	return resp.Payload, nil
   191  }
   192  
   193  func (a *appsCmd) update(c *cli.Context) error {
   194  	appName := c.Args().First()
   195  
   196  	app, err := GetAppByName(a.client, appName)
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	appWithFlags(c, app)
   202  
   203  	if _, err = PutApp(a.client, app.ID, app); err != nil {
   204  		return err
   205  	}
   206  
   207  	fmt.Println("app", appName, "updated")
   208  	return nil
   209  }
   210  
   211  func (a *appsCmd) setConfig(c *cli.Context) error {
   212  	appName := c.Args().Get(0)
   213  	key := c.Args().Get(1)
   214  	value := c.Args().Get(2)
   215  
   216  	app, err := GetAppByName(a.client, appName)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	app.Config = make(map[string]string)
   222  	app.Config[key] = value
   223  
   224  	if _, err = PutApp(a.client, app.ID, app); err != nil {
   225  		return fmt.Errorf("Error updating app configuration: %v", err)
   226  	}
   227  
   228  	fmt.Println(appName, "updated", key, "with", value)
   229  	return nil
   230  }
   231  
   232  func (a *appsCmd) getConfig(c *cli.Context) error {
   233  	appName := c.Args().Get(0)
   234  	key := c.Args().Get(1)
   235  
   236  	app, err := GetAppByName(a.client, appName)
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	val, ok := app.Config[key]
   242  	if !ok {
   243  		return fmt.Errorf("config key does not exist")
   244  	}
   245  
   246  	fmt.Println(val)
   247  
   248  	return nil
   249  }
   250  
   251  func (a *appsCmd) listConfig(c *cli.Context) error {
   252  	appName := c.Args().Get(0)
   253  
   254  	app, err := GetAppByName(a.client, appName)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	if len(app.Config) == 0 {
   260  		fmt.Fprintf(os.Stderr, "No config found for app: %s\n", appName)
   261  		return nil
   262  	}
   263  
   264  	w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
   265  	fmt.Fprint(w, "KEY", "\t", "VALUE", "\n")
   266  	for key, val := range app.Config {
   267  		fmt.Fprint(w, key, "\t", val, "\n")
   268  	}
   269  	w.Flush()
   270  
   271  	return nil
   272  }
   273  
   274  func (a *appsCmd) unsetConfig(c *cli.Context) error {
   275  	appName := c.Args().Get(0)
   276  	key := c.Args().Get(1)
   277  
   278  	app, err := GetAppByName(a.client, appName)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	_, ok := app.Config[key]
   283  	if !ok {
   284  		fmt.Printf("Config key '%s' does not exist. Nothing to do.\n", key)
   285  		return nil
   286  	}
   287  	app.Config[key] = ""
   288  
   289  	_, err = PutApp(a.client, app.ID, app)
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	fmt.Printf("Removed key '%s' from app '%s' \n", key, appName)
   295  	return nil
   296  }
   297  
   298  func (a *appsCmd) inspect(c *cli.Context) error {
   299  	if c.Args().Get(0) == "" {
   300  		return errors.New("Missing app name after the inspect command")
   301  	}
   302  
   303  	appName := c.Args().First()
   304  	prop := c.Args().Get(1)
   305  
   306  	app, err := GetAppByName(a.client, appName)
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	enc := json.NewEncoder(os.Stdout)
   312  	enc.SetIndent("", "\t")
   313  
   314  	if prop == "" {
   315  		enc.Encode(app)
   316  		return nil
   317  	}
   318  
   319  	// TODO: we really need to marshal it here just to
   320  	// unmarshal as map[string]interface{}?
   321  	data, err := json.Marshal(app)
   322  	if err != nil {
   323  		return fmt.Errorf("Could not marshal app: %v", err)
   324  	}
   325  
   326  	var inspect map[string]interface{}
   327  	err = json.Unmarshal(data, &inspect)
   328  	if err != nil {
   329  		return fmt.Errorf("Could not unmarshal data: %v", err)
   330  	}
   331  
   332  	jq := jsonq.NewQuery(inspect)
   333  	field, err := jq.Interface(strings.Split(prop, ".")...)
   334  	if err != nil {
   335  		return fmt.Errorf("Failed to inspect field %v", prop)
   336  	}
   337  	enc.Encode(field)
   338  
   339  	return nil
   340  }
   341  
   342  func (a *appsCmd) delete(c *cli.Context) error {
   343  	appName := c.Args().First()
   344  	if appName == "" {
   345  		//return errors.New("App name required to delete")
   346  	}
   347  
   348  	app, err := GetAppByName(a.client, appName)
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	//recursive delete of sub-objects
   354  	if c.Bool("recursive") {
   355  		fns, triggers, err := common.ListFnsAndTriggersInApp(c, a.client, app)
   356  		if err != nil {
   357  			return fmt.Errorf("Failed to get associated objects: %s", err)
   358  		}
   359  
   360  		//Forced deletion
   361  		var shouldContinue bool
   362  		if c.Bool("force") {
   363  			shouldContinue = true
   364  		} else {
   365  			shouldContinue = common.UserConfirmedMultiResourceDeletion([]*modelsv2.App{app}, fns, triggers)
   366  		}
   367  
   368  		if shouldContinue {
   369  			err := common.DeleteTriggers(c, a.client, triggers)
   370  			if err != nil {
   371  				return fmt.Errorf("Failed to delete associated objects: %s", err)
   372  			}
   373  			err = common.DeleteFunctions(c, a.client, fns)
   374  			if err != nil {
   375  				return fmt.Errorf("Failed to delete associated objects: %s", err)
   376  			}
   377  		} else {
   378  			return nil
   379  		}
   380  	}
   381  
   382  	_, err = a.client.Apps.DeleteApp(&apiapps.DeleteAppParams{
   383  		Context: context.Background(),
   384  		AppID:   app.ID,
   385  	})
   386  
   387  	if err != nil {
   388  		switch e := err.(type) {
   389  		case *apiapps.DeleteAppNotFound:
   390  			return errors.New(e.Payload.Message)
   391  		}
   392  		return err
   393  	}
   394  
   395  	fmt.Println("App", appName, "deleted")
   396  	return nil
   397  }
   398  
   399  // PutApp updates the app with the given ID using the content of the provided app
   400  func PutApp(a *fnclient.Fn, appID string, app *modelsv2.App) (*modelsv2.App, error) {
   401  	resp, err := a.Apps.UpdateApp(&apiapps.UpdateAppParams{
   402  		Context: context.Background(),
   403  		AppID:   appID,
   404  		Body:    app,
   405  	})
   406  
   407  	if err != nil {
   408  		switch e := err.(type) {
   409  		case *apiapps.UpdateAppBadRequest:
   410  			err = fmt.Errorf("%s", e.Payload.Message)
   411  		}
   412  		return nil, err
   413  	}
   414  
   415  	return resp.Payload, nil
   416  }
   417  
   418  // NameNotFoundError error for app not found when looked up by name
   419  type NameNotFoundError struct {
   420  	Name string
   421  }
   422  
   423  func (n NameNotFoundError) Error() string {
   424  	return fmt.Sprintf("app %s not found", n.Name)
   425  }
   426  
   427  // GetAppByName looks up an app by name using the given client
   428  func GetAppByName(client *fnclient.Fn, appName string) (*modelsv2.App, error) {
   429  	appsResp, err := client.Apps.ListApps(&apiapps.ListAppsParams{
   430  		Context: context.Background(),
   431  		Name:    &appName,
   432  	})
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  
   437  	var app *modelsv2.App
   438  	if len(appsResp.Payload.Items) > 0 {
   439  		app = appsResp.Payload.Items[0]
   440  	} else {
   441  		return nil, NameNotFoundError{appName}
   442  	}
   443  
   444  	return app, nil
   445  }