Skip to content

Commit

Permalink
Task/acc 2905/service branch (#328)
Browse files Browse the repository at this point in the history
* more missed ones

* override service branch using cli

* fix

* fix build

* rework

* Revert "rework"

This reverts commit fa1ae17.

* wrap up

* omar's feedback1

* cleanup

* omar's feedback2

* improve err msgs

* omar's feedback

* docs

* extra doc

* better wording

* omar's feedback
  • Loading branch information
Ismail Arafa authored Mar 11, 2024
1 parent 97518c5 commit 8ab3d21
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 6 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ If you want to know more about why we built `tb`, check out our [blog post](http
- [Toggling experimental mode](#toggling-experimental-mode)
- [Adding custom playlists](#adding-custom-playlists)
- [Overriding service properties](#overriding-service-properties)
- [Overriding Remote Tag using CLI](#overriding-remote-tag-using-cli)
- [Contributing](#contributing)
- [License](#license)

Expand Down Expand Up @@ -180,6 +181,34 @@ Override schema:
tag: string # The image tag to use
```

#### Overriding Remote Tag using CLI

Additionally, you can run a docker image with a specific remote tag using the CLI. An example of doing so for a single service looks like this:

```
sso tb up my-service -t my_tag
```

Alternatively, you could also use the following syntax when running a single service:

```
sso tb up my-service -t my-service:my_tag
```

You can also override a remote tag when running a playlist. To override multiple remote tags for multiple services, you will need to provide a comma-separated list of service:tag arguments.

```
sso tb up -p my-playlist -t postgres:my_tag1,redis:my_tag2
```
Additionally, you can override multiple services' tags when a list of services:
```
sso tb up my-service my-service1 -t my-service:my_tag1,my-service:my_tag2
```
NOTE: If you specify a remote tag in your `.tbrc.yml` and supply a tag using the CLI, the CLI tag will always takes precedence over the `.tbrc.yml` override.
## Contributing
See [contributing](CONTRIBUTING.md) for instructions on how to contribute to `tb`. PRs welcome!
Expand Down
37 changes: 37 additions & 0 deletions cli/commands/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"fmt"
"os/exec"
"strings"

"github.com/TouchBistro/goutils/command"
"github.com/TouchBistro/goutils/fatal"
Expand All @@ -18,6 +19,28 @@ type upOptions struct {
skipLazydocker bool
playlistName string
serviceNames []string
serviceTags []string
}

func parseTag(tag string, serviceNames []string) ([]string, error) {
parts := strings.Split(tag, ":")
// when running one service with one part ex: tb up some-service -t some-tag
if len(serviceNames) == 1 && len(parts) == 1 {
return []string{serviceNames[0], parts[0]}, nil

}

// split the service tag into service name and tag, ensuring exactly two string values
// ex: tb up some-service -t some-service:some-tag
if len(parts) != 2 {
return []string{}, fmt.Errorf("invalid service tag format '%s'; expected format 'service:tag'", tag)
}

if parts[0] == "" || parts[1] == "" {
return []string{}, fmt.Errorf("invalid service tag '%s'; service name and tag must not be empty", tag)
}

return parts, nil
}

func newUpCommand(c *cli.Container) *cobra.Command {
Expand Down Expand Up @@ -69,13 +92,26 @@ Run the postgres and localstack services directly:
if len(serviceNames) == 0 {
serviceNames = opts.serviceNames
}

serviceTags := make(map[string]string)
for _, serviceTag := range opts.serviceTags {
parts, err := parseTag(serviceTag, serviceNames)
if err != nil {
return &fatal.Error{
Msg: fmt.Sprintf("Failed to parse service tag %s", serviceTag),
Err: err,
}
}
serviceTags[parts[0]] = parts[1]
}
err := c.Engine.Up(c.Ctx, engine.UpOptions{
ServiceNames: serviceNames,
PlaylistName: opts.playlistName,
SkipPreRun: opts.skipServicePreRun,
SkipDockerPull: opts.skipDockerPull,
SkipGitPull: opts.skipGitPull,
OfflineMode: c.OfflineMode,
ServiceTags: serviceTags,
})
if err != nil {
return &fatal.Error{
Expand Down Expand Up @@ -113,6 +149,7 @@ Run the postgres and localstack services directly:
flags.BoolVar(&opts.skipDockerPull, "no-remote-pull", false, "Don't get new remote images")
flags.BoolVar(&opts.skipLazydocker, "no-lazydocker", false, "Don't start lazydocker")
flags.StringVarP(&opts.playlistName, "playlist", "p", "", "The name of a playlist")
flags.StringSliceVarP(&opts.serviceTags, "image-tag", "t", []string{}, "Comma separated list of service:image-tag to run")
flags.StringSliceVarP(&opts.serviceNames, "services", "s", []string{}, "Comma separated list of services to start. eg --services postgres,localstack.")
err := flags.MarkDeprecated("services", "and will be removed, pass service names as arguments instead")
if err != nil {
Expand Down
45 changes: 39 additions & 6 deletions engine/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func (e *Engine) ResolveService(serviceName string) (service.Service, error) {
type UpOptions struct {
// ServiceNames is a list of services names to start.
ServiceNames []string
// ServiceTags is a map of service:image-tag to start.
ServiceTags map[string]string
// PlaylistName is the name of a playlist to start.
PlaylistName string
// SkipPreRun skips running the pre-run step for services.
Expand Down Expand Up @@ -65,7 +67,7 @@ type UpOptions struct {
// which services to start.
func (e *Engine) Up(ctx context.Context, opts UpOptions) error {
const op = errors.Op("engine.Engine.Up")
services, err := e.resolveServices(op, opts.ServiceNames, opts.PlaylistName, true)
services, err := e.resolveServices(op, opts.ServiceNames, opts.PlaylistName, opts.ServiceTags, true)
if err != nil {
return err
}
Expand Down Expand Up @@ -251,7 +253,7 @@ type DownOptions struct {
// Down stops services and removes the containers.
func (e *Engine) Down(ctx context.Context, opts DownOptions) error {
const op = errors.Op("engine.Engine.Down")
services, err := e.resolveServices(op, opts.ServiceNames, "", false)
services, err := e.resolveServices(op, opts.ServiceNames, "", make(map[string]string), false)
if err != nil {
return err
}
Expand Down Expand Up @@ -282,7 +284,7 @@ type LogsOptions struct {
// Logs retrieves the logs from one or more service containers and writes it to w.
func (e *Engine) Logs(ctx context.Context, w io.Writer, opts LogsOptions) error {
const op = errors.Op("engine.Engine.Logs")
services, err := e.resolveServices(op, opts.ServiceNames, "", false)
services, err := e.resolveServices(op, opts.ServiceNames, "", make(map[string]string), false)
if err != nil {
return err
}
Expand Down Expand Up @@ -562,25 +564,56 @@ func (e *Engine) nuke(ctx context.Context, opts NukeOptions, op errors.Op) error

// resolveServices resolves a list of services from either a list of service names or a playlist name.
//
// If both serivceNames and playlistName are provided, an error will be returned. Mixing service names
// If both serviceNames and playlistName are provided, an error will be returned. Mixing service names
// is not supported.
//
// If neither serviceNames nor playlistName are provided, then behaviour depends on the value of requireOne.
// In this case, if requireOne is true, an error will be returned since at least one of serviceNames or playlistName
// was required. Otherwise, both the returned slice and error will be nil, which can be treated as an empty slice
// of services.
func (e *Engine) resolveServices(op errors.Op, serviceNames []string, playlistName string, requireOne bool) ([]service.Service, error) {
func (e *Engine) resolveServices(op errors.Op, serviceNames []string, playlistName string, serviceTags map[string]string, requireOne bool) ([]service.Service, error) {
if len(serviceNames) > 0 && playlistName != "" {
return nil, errors.New(errkind.Invalid, "both service names and playlist name provided", op)
}
if len(serviceNames) > 0 {
for service, tag := range serviceTags {
_, err := e.services.Get(service)
if err != nil {
return nil, errors.Wrap(err, errors.Meta{Reason: fmt.Sprintf("unable to resolve service %s with tag %s", service, tag), Op: op})
}
}

services := make([]service.Service, len(serviceNames))
for i, name := range serviceNames {
s, err := e.services.Get(name)
if err != nil {
return nil, errors.Wrap(err, errors.Meta{Reason: "unable to resolve service", Op: op})
}

services[i] = s
tag := serviceTags[s.Name]
if len(tag) > 0 {
override := service.ServiceOverride{
Mode: "remote",
Remote: service.RemoteOverride{
Tag: tag,
},
}
overridenService, err := service.Override(s, override)
if err != nil {
return nil, errors.Wrap(err, errors.Meta{Reason: fmt.Sprintf("failed to override service %s with tag %s", s.Name, tag), Op: op})
}

/* e.services is the global list of services parsed from the registry
* we need to update its state with the given remote tag
* since it's used downstream to generate the docker-compose config to tag the service
*/
if err := e.services.Set(overridenService); err != nil {
return nil, errors.Wrap(err, errors.Meta{Reason: fmt.Sprintf("failed to override service %s with tag %s", s.Name, tag), Op: op})
}

services[i] = overridenService
}
}
return services, nil
}
Expand All @@ -590,7 +623,7 @@ func (e *Engine) resolveServices(op errors.Op, serviceNames []string, playlistNa
return nil, errors.Wrap(err, errors.Meta{Reason: "unable to resolve playlist", Op: op})
}
// Can just run resolveServices again with the service names to get the actual services.
return e.resolveServices(op, serviceNames, "", true)
return e.resolveServices(op, serviceNames, "", serviceTags, true)
}
if requireOne {
return nil, errors.New(errkind.Invalid, "neither service names nor playlist name was provided", op)
Expand Down

0 comments on commit 8ab3d21

Please sign in to comment.