github.com/rigado/snapd@v2.42.5-go-mod+incompatible/client/snap_op.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package client
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"mime/multipart"
    28  	"os"
    29  	"path/filepath"
    30  )
    31  
    32  type SnapOptions struct {
    33  	Channel          string `json:"channel,omitempty"`
    34  	Revision         string `json:"revision,omitempty"`
    35  	CohortKey        string `json:"cohort-key,omitempty"`
    36  	LeaveCohort      bool   `json:"leave-cohort,omitempty"`
    37  	DevMode          bool   `json:"devmode,omitempty"`
    38  	JailMode         bool   `json:"jailmode,omitempty"`
    39  	Classic          bool   `json:"classic,omitempty"`
    40  	Dangerous        bool   `json:"dangerous,omitempty"`
    41  	IgnoreValidation bool   `json:"ignore-validation,omitempty"`
    42  	Unaliased        bool   `json:"unaliased,omitempty"`
    43  	Purge            bool   `json:"purge,omitempty"`
    44  	Amend            bool   `json:"amend,omitempty"`
    45  
    46  	Users []string `json:"users,omitempty"`
    47  }
    48  
    49  func writeFieldBool(mw *multipart.Writer, key string, val bool) error {
    50  	if !val {
    51  		return nil
    52  	}
    53  	return mw.WriteField(key, "true")
    54  }
    55  
    56  func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error {
    57  	fields := []struct {
    58  		f string
    59  		b bool
    60  	}{
    61  		{"devmode", opts.DevMode},
    62  		{"classic", opts.Classic},
    63  		{"jailmode", opts.JailMode},
    64  		{"dangerous", opts.Dangerous},
    65  	}
    66  	for _, o := range fields {
    67  		if err := writeFieldBool(mw, o.f, o.b); err != nil {
    68  			return err
    69  		}
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error {
    76  	return writeFieldBool(mw, "unaliased", opts.Unaliased)
    77  }
    78  
    79  type actionData struct {
    80  	Action   string `json:"action"`
    81  	Name     string `json:"name,omitempty"`
    82  	SnapPath string `json:"snap-path,omitempty"`
    83  	*SnapOptions
    84  }
    85  
    86  type multiActionData struct {
    87  	Action string   `json:"action"`
    88  	Snaps  []string `json:"snaps,omitempty"`
    89  	Users  []string `json:"users,omitempty"`
    90  }
    91  
    92  // Install adds the snap with the given name from the given channel (or
    93  // the system default channel if not).
    94  func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) {
    95  	return client.doSnapAction("install", name, options)
    96  }
    97  
    98  func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) {
    99  	return client.doMultiSnapAction("install", names, options)
   100  }
   101  
   102  // Remove removes the snap with the given name.
   103  func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) {
   104  	return client.doSnapAction("remove", name, options)
   105  }
   106  
   107  func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) {
   108  	return client.doMultiSnapAction("remove", names, options)
   109  }
   110  
   111  // Refresh refreshes the snap with the given name (switching it to track
   112  // the given channel if given).
   113  func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) {
   114  	return client.doSnapAction("refresh", name, options)
   115  }
   116  
   117  func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) {
   118  	return client.doMultiSnapAction("refresh", names, options)
   119  }
   120  
   121  func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) {
   122  	return client.doSnapAction("enable", name, options)
   123  }
   124  
   125  func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) {
   126  	return client.doSnapAction("disable", name, options)
   127  }
   128  
   129  // Revert rolls the snap back to the previous on-disk state
   130  func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) {
   131  	return client.doSnapAction("revert", name, options)
   132  }
   133  
   134  // Switch moves the snap to a different channel without a refresh
   135  func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) {
   136  	return client.doSnapAction("switch", name, options)
   137  }
   138  
   139  // SnapshotMany snapshots many snaps (all, if names empty) for many users (all, if users is empty).
   140  func (client *Client) SnapshotMany(names []string, users []string) (setID uint64, changeID string, err error) {
   141  	result, changeID, err := client.doMultiSnapActionFull("snapshot", names, &SnapOptions{Users: users})
   142  	if err != nil {
   143  		return 0, "", err
   144  	}
   145  	if len(result) == 0 {
   146  		return 0, "", fmt.Errorf("server result does not contain snapshot set identifier")
   147  	}
   148  	var x struct {
   149  		SetID uint64 `json:"set-id"`
   150  	}
   151  	if err := json.Unmarshal(result, &x); err != nil {
   152  		return 0, "", err
   153  	}
   154  	return x.SetID, changeID, nil
   155  }
   156  
   157  var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file")
   158  
   159  func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) {
   160  	if options != nil && options.Dangerous {
   161  		return "", ErrDangerousNotApplicable
   162  	}
   163  	action := actionData{
   164  		Action:      actionName,
   165  		SnapOptions: options,
   166  	}
   167  	data, err := json.Marshal(&action)
   168  	if err != nil {
   169  		return "", fmt.Errorf("cannot marshal snap action: %s", err)
   170  	}
   171  	path := fmt.Sprintf("/v2/snaps/%s", snapName)
   172  
   173  	headers := map[string]string{
   174  		"Content-Type": "application/json",
   175  	}
   176  
   177  	return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data))
   178  }
   179  
   180  func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) {
   181  	if options != nil {
   182  		return "", fmt.Errorf("cannot use options for multi-action") // (yet)
   183  	}
   184  	_, changeID, err = client.doMultiSnapActionFull(actionName, snaps, options)
   185  
   186  	return changeID, err
   187  }
   188  
   189  func (client *Client) doMultiSnapActionFull(actionName string, snaps []string, options *SnapOptions) (result json.RawMessage, changeID string, err error) {
   190  	action := multiActionData{
   191  		Action: actionName,
   192  		Snaps:  snaps,
   193  	}
   194  	if options != nil {
   195  		action.Users = options.Users
   196  	}
   197  	data, err := json.Marshal(&action)
   198  	if err != nil {
   199  		return nil, "", fmt.Errorf("cannot marshal multi-snap action: %s", err)
   200  	}
   201  
   202  	headers := map[string]string{
   203  		"Content-Type": "application/json",
   204  	}
   205  
   206  	return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data))
   207  }
   208  
   209  // InstallPath sideloads the snap with the given path under optional provided name,
   210  // returning the UUID of the background operation upon success.
   211  func (client *Client) InstallPath(path, name string, options *SnapOptions) (changeID string, err error) {
   212  	f, err := os.Open(path)
   213  	if err != nil {
   214  		return "", fmt.Errorf("cannot open: %q", path)
   215  	}
   216  
   217  	action := actionData{
   218  		Action:      "install",
   219  		Name:        name,
   220  		SnapPath:    path,
   221  		SnapOptions: options,
   222  	}
   223  
   224  	pr, pw := io.Pipe()
   225  	mw := multipart.NewWriter(pw)
   226  	go sendSnapFile(path, f, pw, mw, &action)
   227  
   228  	headers := map[string]string{
   229  		"Content-Type": mw.FormDataContentType(),
   230  	}
   231  
   232  	return client.doAsync("POST", "/v2/snaps", nil, headers, pr)
   233  }
   234  
   235  // Try
   236  func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) {
   237  	if options == nil {
   238  		options = &SnapOptions{}
   239  	}
   240  	if options.Dangerous {
   241  		return "", ErrDangerousNotApplicable
   242  	}
   243  
   244  	buf := bytes.NewBuffer(nil)
   245  	mw := multipart.NewWriter(buf)
   246  	mw.WriteField("action", "try")
   247  	mw.WriteField("snap-path", path)
   248  	options.writeModeFields(mw)
   249  	mw.Close()
   250  
   251  	headers := map[string]string{
   252  		"Content-Type": mw.FormDataContentType(),
   253  	}
   254  
   255  	return client.doAsync("POST", "/v2/snaps", nil, headers, buf)
   256  }
   257  
   258  func sendSnapFile(snapPath string, snapFile *os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) {
   259  	defer snapFile.Close()
   260  
   261  	if action.SnapOptions == nil {
   262  		action.SnapOptions = &SnapOptions{}
   263  	}
   264  	fields := []struct {
   265  		name  string
   266  		value string
   267  	}{
   268  		{"action", action.Action},
   269  		{"name", action.Name},
   270  		{"snap-path", action.SnapPath},
   271  		{"channel", action.Channel},
   272  	}
   273  	for _, s := range fields {
   274  		if s.value == "" {
   275  			continue
   276  		}
   277  		if err := mw.WriteField(s.name, s.value); err != nil {
   278  			pw.CloseWithError(err)
   279  			return
   280  		}
   281  	}
   282  
   283  	if err := action.writeModeFields(mw); err != nil {
   284  		pw.CloseWithError(err)
   285  		return
   286  	}
   287  
   288  	if err := action.writeOptionFields(mw); err != nil {
   289  		pw.CloseWithError(err)
   290  		return
   291  	}
   292  
   293  	fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath))
   294  	if err != nil {
   295  		pw.CloseWithError(err)
   296  		return
   297  	}
   298  
   299  	_, err = io.Copy(fw, snapFile)
   300  	if err != nil {
   301  		pw.CloseWithError(err)
   302  		return
   303  	}
   304  
   305  	mw.Close()
   306  	pw.Close()
   307  }