Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for install/uninstall containers #228

Merged
merged 4 commits into from
Aug 30, 2022
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
162 changes: 162 additions & 0 deletions cmd/jag/commands/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (C) 2022 Toitware ApS. All rights reserved.
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.

package commands

import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
"github.com/toitlang/jaguar/cmd/jag/directory"
)

func ContainerCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "container",
Short: "Manipulate the set of installed containers on a device",
Long: "Manipulate the set of installed containers on a device.\n" +
"Installed containers run on boot and are primarily used to provide\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Installed containers run on boot and are primarily used to provide\n" +
"Installed containers are run on boot and are primarily used to provide\n" +

"services and drivers to applications.",
}

cmd.AddCommand(ContainerListCmd())
cmd.AddCommand(ContainerInstallCmd())
cmd.AddCommand(ContainerUninstallCmd())
return cmd
}

func ContainerListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := directory.GetDeviceConfig()
if err != nil {
return err
}

deviceSelect, err := parseDeviceFlag(cmd)
if err != nil {
return err
}

ctx := cmd.Context()
sdk, err := GetSDK(ctx)
if err != nil {
return err
}

device, err := GetDevice(ctx, cfg, sdk, false, deviceSelect)
if err != nil {
return err
}

containers, err := device.ContainerList(ctx, sdk)
if err != nil {
return err
}

for id, name := range containers {
fmt.Println(id + ": " + name)
}

return nil
},
}

cmd.Flags().StringP("device", "d", "", "use device with a given name, id, or address")
return cmd
}

func ContainerInstallCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "install <name> <file>",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

deviceSelect, err := parseDeviceFlag(cmd)
if err != nil {
return err
}

cfg, err := directory.GetDeviceConfig()
if err != nil {
return err
}

entrypoint := args[1]
if stat, err := os.Stat(entrypoint); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("no such file or directory: '%s'", entrypoint)
}
return fmt.Errorf("can't stat file '%s', reason: %w", entrypoint, err)
} else if stat.IsDir() {
return fmt.Errorf("can't run directory: '%s'", entrypoint)
}

sdk, err := GetSDK(ctx)
if err != nil {
return err
}

device, err := GetDevice(ctx, cfg, sdk, true, deviceSelect)
if err != nil {
return err
}

name := args[0]
fmt.Printf("Installing container '%s' from '%s' on '%s' ...\n", name, entrypoint, device.Name)
return RunFile(cmd, device, sdk, entrypoint, containerRunOptions(name))
},
}

cmd.Flags().StringP("device", "d", "", "use device with a given name, id, or address")
return cmd
}

func ContainerUninstallCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "uninstall <name>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

deviceSelect, err := parseDeviceFlag(cmd)
if err != nil {
return err
}

cfg, err := directory.GetDeviceConfig()
if err != nil {
return err
}

sdk, err := GetSDK(ctx)
if err != nil {
return err
}

device, err := GetDevice(ctx, cfg, sdk, true, deviceSelect)
if err != nil {
return err
}

name := args[0]
fmt.Printf("Uninstalling container '%s' on '%s' ...\n", name, device.Name)
return device.ContainerUninstall(ctx, sdk, containerRunOptions(name))
},
}

cmd.Flags().StringP("device", "d", "", "use device with a given name, id, or address")
return cmd
}

func containerRunOptions(name string) string {
escapedName := strings.ReplaceAll(name, "\"", "\\\"")
runOptions := "{ \"container.name\": \"" + escapedName + "\" }"
return runOptions
}
52 changes: 52 additions & 0 deletions cmd/jag/commands/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -96,6 +97,57 @@ func (d Device) Run(ctx context.Context, sdk *SDK, b []byte, defines string) err
return nil
}

func (d Device) ContainerList(ctx context.Context, sdk *SDK) (map[string]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", d.Address+"/list", nil)
if err != nil {
return nil, err
}
req.Header.Set(JaguarDeviceIDHeader, d.ID)
req.Header.Set(JaguarSDKVersionHeader, sdk.Version)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("got non-OK from device: %s", res.Status)
}

var unmarshalled map[string]string
if err = json.Unmarshal(body, &unmarshalled); err != nil {
return nil, err
}

return unmarshalled, nil
}

func (d Device) ContainerUninstall(ctx context.Context, sdk *SDK, defines string) error {
req, err := http.NewRequestWithContext(ctx, "PUT", d.Address+"/uninstall", nil)
if err != nil {
return err
}
req.Header.Set(JaguarDeviceIDHeader, d.ID)
req.Header.Set(JaguarSDKVersionHeader, sdk.Version)
req.Header.Set(JaguarRunDefinesHeader, defines)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

io.ReadAll(res.Body) // Avoid closing connection prematurely.
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("got non-OK from device: %s", res.Status)
}
return nil
}

// A Reader based on a byte array that prints a progress bar.
type ProgressReader struct {
b []byte
Expand Down
1 change: 1 addition & 0 deletions cmd/jag/commands/jag.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func JagCmd(info Info, isReleaseBuild bool) *cobra.Command {

cmd.AddCommand(
ScanCmd(),
ContainerCmd(),
PingCmd(),
RunCmd(),
CompileCmd(),
Expand Down
2 changes: 1 addition & 1 deletion cmd/jag/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func RunCmd() *cobra.Command {
if err != nil {
return err
}
fmt.Printf("Running '%s' on '%s' ...\n", entrypoint, device.Name)
return RunFile(cmd, device, sdk, entrypoint, runOptions)
},
}
Expand Down Expand Up @@ -177,7 +178,6 @@ func runOnHost(ctx context.Context, cmd *cobra.Command, args []string) error {
}

func RunFile(cmd *cobra.Command, device *Device, sdk *SDK, path string, defines string) error {
fmt.Printf("Running '%s' on '%s' ...\n", path, device.Name)
ctx := cmd.Context()

snapshotsCache, err := directory.GetSnapshotsCachePath()
Expand Down
13 changes: 11 additions & 2 deletions cmd/jag/commands/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,12 @@ func parseRunDefinesFlags(cmd *cobra.Command, flagName string) (string, error) {
definesMap := make(map[string]interface{})
for _, element := range definesFlags {
indexOfAssign := strings.Index(element, "=")
var key string
if indexOfAssign < 0 {
key := strings.TrimSpace(element)
key = strings.TrimSpace(element)
definesMap[key] = true
} else {
key := strings.TrimSpace(element[0:indexOfAssign])
key = strings.TrimSpace(element[0:indexOfAssign])
value := strings.TrimSpace(element[indexOfAssign+1:])

// Try to parse the value as a JSON value and avoid turning
Expand All @@ -357,6 +358,14 @@ func parseRunDefinesFlags(cmd *cobra.Command, flagName string) (string, error) {
definesMap[key] = value
}
}
if key == "run.boot" {
fmt.Println()
fmt.Println("*********************************************")
fmt.Println("* Using 'jag run -D run.boot' is deprecated *")
fmt.Println("* .. use 'jag container install' instead .. *")
fmt.Println("*********************************************")
fmt.Println()
}
}
if len(definesMap) == 0 {
return "", nil
Expand Down
1 change: 1 addition & 0 deletions cmd/jag/commands/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func onWatchChanges(cmd *cobra.Command, watcher *watcher, device *Device, sdk *S
}

runOnDevice := func(runCtx context.Context) {
fmt.Printf("Running '%s' on '%s' ...\n", entrypoint, device.Name)
if err := RunFile(cmd, device, sdk, entrypoint, ""); err != nil {
fmt.Println("Error:", err)
return
Expand Down
85 changes: 85 additions & 0 deletions src/container_registry.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (C) 2022 Toitware ApS. All rights reserved.
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.

import device
import uuid
import system.containers

flash_ ::= device.FlashStore
jaguar_ / uuid.Uuid ::= containers.current

class ContainerRegistry:
static KEY_ /string ::= "jag.containers"

loaded_ / bool := false
id_by_name_ / Map ::= {:} // Map<string, uuid.Uuid>
name_by_id_ / Map ::= {:} // Map<uuid.Uuid, string>

entries -> Map:
ensure_loaded_
result := {:}
name_by_id_.do: | id/uuid.Uuid name/string | result["$id"] = name
return result

install name/string? [block] -> uuid.Uuid:
ensure_loaded_
// Uninstall all unnamed images. This is used to prepare
// for running another unnamed image.
images/List ::= containers.images
images.do: | id/uuid.Uuid |
if not name_by_id_.contains id:
containers.uninstall id
if name: uninstall name
// Now actually create the image by invoking the block.
image := block.call
if not name: return image
// Update the name mapping and make sure we do not have
// an old name for the same image floating around.
old := name_by_id_.get image
if old: id_by_name_.remove old
name_by_id_[image] = name
id_by_name_[name] = image
store_
return image

uninstall name/string -> uuid.Uuid?:
ensure_loaded_
id := id_by_name_.get name --if_absent=: return null
containers.uninstall id
id_by_name_.remove name
name_by_id_.remove id
store_
return id

ensure_loaded_ -> none:
if loaded_: return
dirty := true
entries := {:}
catch --trace:
entries = flash_.get KEY_
dirty = false
// Run through the images actually installed in flash and update the
// registry accordingly. This involves inventing names for unexpected
// containers found in flash and pruning names for containers that
// we cannot find anymore.
index := 0
images/List ::= containers.images
images.do: | id/uuid.Uuid |
name/string? := null
kasperl marked this conversation as resolved.
Show resolved Hide resolved
// We are not sure that the entries loaded from flash is a map
// with the correct structure, so we guard the access to the
// individual entries and treat malformed ones as non-existing.
catch: name = entries.get "$id"
if not name:
name = (id == jaguar_) ? "jaguar" : "container-$(index++)"
dirty = true
id_by_name_[name] = id
name_by_id_[id] = name
// We're done loading. If we've changed the name mapping in any way,
// we write the updated entries back into flash.
loaded_ = true
if dirty or entries.size > images.size: store_

store_ -> none:
flash_.set KEY_ entries
Loading