pulumi/cmd/logs.go

205 lines
6.2 KiB
Go
Raw Normal View History

2018-05-22 21:43:36 +02:00
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"strings"
"time"
mobytime "github.com/docker/docker/api/types/time"
2017-11-28 21:54:36 +01:00
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/backend/display"
"github.com/pulumi/pulumi/pkg/diag/colors"
2017-11-20 07:28:49 +01:00
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
// We use RFC 5424 timestamps with millisecond precision for displaying time stamps on log entries. Go does not
// pre-define a format string for this format, though it is similar to time.RFC3339Nano.
//
// See https://tools.ietf.org/html/rfc5424#section-6.2.3.
const timeFormat = "2006-01-02T15:04:05.000Z07:00"
func newLogsCmd() *cobra.Command {
var stack string
var follow bool
var since string
2017-11-23 05:58:46 +01:00
var resource string
var jsonOut bool
logsCmd := &cobra.Command{
Use: "logs",
Short: "[PREVIEW] Show aggregated logs for a stack",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
opts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
Initial support for passing URLs to `new` and `up` (#1727) * Initial support for passing URLs to `new` and `up` This PR adds initial support for `pulumi new` using Git under the covers to manage Pulumi templates, providing the same experience as before. You can now also optionally pass a URL to a Git repository, e.g. `pulumi new [<url>]`, including subdirectories within the repository, and arbitrary branches, tags, or commits. The following commands result in the same behavior from the user's perspective: - `pulumi new javascript` - `pulumi new https://github.com/pulumi/templates/templates/javascript` - `pulumi new https://github.com/pulumi/templates/tree/master/templates/javascript` - `pulumi new https://github.com/pulumi/templates/tree/HEAD/templates/javascript` To specify an arbitrary branch, tag, or commit: - `pulumi new https://github.com/pulumi/templates/tree/<branch>/templates/javascript` - `pulumi new https://github.com/pulumi/templates/tree/<tag>/templates/javascript` - `pulumi new https://github.com/pulumi/templates/tree/<commit>/templates/javascript` Branches and tags can include '/' separators, and `pulumi` will still find the right subdirectory. URLs to Gists are also supported, e.g.: `pulumi new https://gist.github.com/justinvp/6673959ceb9d2ac5a14c6d536cb871a6` If the specified subdirectory in the repository does not contain a `Pulumi.yaml`, it will look for subdirectories within containing `Pulumi.yaml` files, and prompt the user to choose a template, along the lines of how `pulumi new` behaves when no template is specified. The following commands result in the CLI prompting to choose a template: - `pulumi new` - `pulumi new https://github.com/pulumi/templates/templates` - `pulumi new https://github.com/pulumi/templates/tree/master/templates` - `pulumi new https://github.com/pulumi/templates/tree/HEAD/templates` Of course, arbitrary branches, tags, or commits can be specified as well: - `pulumi new https://github.com/pulumi/templates/tree/<branch>/templates` - `pulumi new https://github.com/pulumi/templates/tree/<tag>/templates` - `pulumi new https://github.com/pulumi/templates/tree/<commit>/templates` This PR also includes initial support for passing URLs to `pulumi up`, providing a streamlined way to deploy installable cloud applications with Pulumi, without having to manage source code locally before doing a deployment. For example, `pulumi up https://github.com/justinvp/aws` can be used to deploy a sample AWS app. The stack can be updated with different versions, e.g. `pulumi up https://github.com/justinvp/aws/tree/v2 -s <stack-to-update>` Config values can optionally be passed via command line flags, e.g. `pulumi up https://github.com/justinvp/aws -c aws:region=us-west-2 -c foo:bar=blah` Gists can also be used, e.g. `pulumi up https://gist.github.com/justinvp/62fde0463f243fcb49f5a7222e51bc76` * Fix panic when hitting ^C from "choose template" prompt * Add description to templates When running `pulumi new` without specifying a template, include the template description along with the name in the "choose template" display. ``` $ pulumi new Please choose a template: aws-go A minimal AWS Go program aws-javascript A minimal AWS JavaScript program aws-python A minimal AWS Python program aws-typescript A minimal AWS TypeScript program > go A minimal Go program hello-aws-javascript A simple AWS serverless JavaScript program javascript A minimal JavaScript program python A minimal Python program typescript A minimal TypeScript program ``` * React to changes to the pulumi/templates repo. We restructured the `pulumi/templates` repo to have all the templates in the root instead of in a `templates` subdirectory, so make the change here to no longer look for templates in `templates`. This also fixes an issue around using `Depth: 1` that I found while testing this. When a named template is used, we attempt to clone or pull from the `pulumi/templates` repo to `~/.pulumi/templates`. Having it go in this well-known directory allows us to maintain previous behavior around allowing offline use of templates. If we use `Depth: 1` for the initial clone, it will fail when attempting to pull when there are updates to the remote repository. Unfortunately, there's no built-in `--unshallow` support in `go-git` and setting a larger `Depth` doesn't appear to help. There may be a workaround, but for now, if we're cloning the pulumi templates directory to `~/.pulumi/templates`, we won't use `Depth: 1`. For template URLs, we will continue to use `Depth: 1` as we clone those to a temp directory (which gets deleted) that we'll never try to update. * List available templates in help text * Address PR Feedback * Don't show "Installing dependencies" message for `up` * Fix secrets handling When prompting for config, if the existing stack value is a secret, keep it a secret and mask the prompt. If the template says it should be secret, make it a secret. * Fix ${PROJECT} and ${DESCRIPTION} handling for `up` Templates used with `up` should already have a filled-in project name and description, but if it's a `new`-style template, that has `${PROJECT}` and/or `${DESCRIPTION}`, be helpful and just replace these with better values. * Fix stack handling Add a bool `setCurrent` param to `requireStack` to control whether the current stack should be saved in workspace settings. For the `up <url>` case, we don't want to save. Also, split the `up` code into two separate functions: one for the `up <url>` case and another for the normal `up` case where you have workspace in your current directory. While we may be able to combine them back into a single function, right now it's a bit cleaner being separate, even with some small amount of duplication. * Fix panic due to nil crypter Lazily get the crypter only if needed inside `promptForConfig`. * Embellish comment * Harden isPreconfiguredEmptyStack check Fix the code to check to make sure the URL specified on the command line matches the URL stored in the `pulumi:template` config value, and that the rest of the config from the stack satisfies the config requirements of the template.
2018-08-11 03:08:16 +02:00
s, err := requireStack(stack, false, opts, true /*setCurrent*/)
if err != nil {
return err
}
sm, err := getStackSecretsManager(s)
if err != nil {
return errors.Wrap(err, "getting secrets manager")
}
cfg, err := getStackConfiguration(s, sm)
if err != nil {
return errors.Wrap(err, "getting stack configuration")
}
startTime, err := parseSince(since, time.Now())
2017-11-28 21:54:36 +01:00
if err != nil {
return errors.Wrapf(err, "failed to parse argument to '--since' as duration or timestamp")
}
2017-11-23 05:58:46 +01:00
var resourceFilter *operations.ResourceFilter
if resource != "" {
var rf = operations.ResourceFilter(resource)
resourceFilter = &rf
}
if !jsonOut {
fmt.Printf(
opts.Color.Colorize(colors.BrightMagenta+"Collecting logs for stack %s since %s.\n\n"+colors.Reset),
s.Ref().String(),
startTime.Format(timeFormat),
)
}
// IDEA: This map will grow forever as new log entries are found. We may need to do a more approximate
// approach here to ensure we don't grow memory unboundedly while following logs.
//
// Note: Just tracking latest log date is not sufficient - as stale logs may show up which should have been
// displayed before previously rendered log entries, but weren't available at the time, so still need to be
// rendered now even though they are technically out of order.
shown := map[operations.LogEntry]bool{}
for {
logs, err := s.GetLogs(commandContext(), cfg, operations.LogQuery{
StartTime: startTime,
ResourceFilter: resourceFilter,
})
if err != nil {
2017-11-28 21:54:36 +01:00
return errors.Wrapf(err, "failed to get logs")
}
// When we are emitting a fixed number of log entries, and outputing JSON, wrap them in an array.
if !follow && jsonOut {
entries := make([]logEntryJSON, 0, len(logs))
for _, logEntry := range logs {
if _, shownAlready := shown[logEntry]; !shownAlready {
eventTime := time.Unix(0, logEntry.Timestamp*1000000)
entries = append(entries, logEntryJSON{
ID: logEntry.ID,
Timestamp: eventTime.UTC().Format(timeFormat),
Message: logEntry.Message,
})
shown[logEntry] = true
}
}
return printJSON(entries)
}
for _, logEntry := range logs {
if _, shownAlready := shown[logEntry]; !shownAlready {
eventTime := time.Unix(0, logEntry.Timestamp*1000000)
if !jsonOut {
fmt.Printf(
"%30.30s[%30.30s] %v\n",
eventTime.Format(timeFormat),
logEntry.ID,
strings.TrimRight(logEntry.Message, "\n"),
)
} else {
err = printJSON(logEntryJSON{
ID: logEntry.ID,
Timestamp: eventTime.UTC().Format(timeFormat),
Message: logEntry.Message,
})
if err != nil {
return err
}
}
shown[logEntry] = true
}
}
if !follow {
return nil
}
time.Sleep(time.Second)
}
}),
}
logsCmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
logsCmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
logsCmd.PersistentFlags().BoolVarP(
&jsonOut, "json", "j", false, "Emit output as JSON")
logsCmd.PersistentFlags().BoolVarP(
&follow, "follow", "f", false,
"Follow the log stream in real time (like tail -f)")
logsCmd.PersistentFlags().StringVar(
&since, "since", "1h",
"Only return logs newer than a relative duration ('5s', '2m', '3h') or absolute timestamp. "+
"Defaults to returning the last 1 hour of logs.")
2017-11-23 05:58:46 +01:00
logsCmd.PersistentFlags().StringVarP(
&resource, "resource", "r", "",
"Only return logs for the requested resource ('name', 'type::name' or full URN). Defaults to returning all logs.")
return logsCmd
}
func parseSince(since string, reference time.Time) (*time.Time, error) {
startTimestamp, err := mobytime.GetTimestamp(since, reference)
2017-11-28 21:54:36 +01:00
if err != nil {
return nil, err
}
2017-11-28 21:54:36 +01:00
startTimeSec, startTimeNs, err := mobytime.ParseTimestamps(startTimestamp, 0)
if err != nil {
2017-11-28 21:54:36 +01:00
return nil, err
}
2017-11-28 21:54:36 +01:00
if startTimeSec == 0 && startTimeNs == 0 {
return nil, nil
}
2017-11-28 21:54:36 +01:00
startTime := time.Unix(startTimeSec, startTimeNs)
return &startTime, nil
}
// logEntryJSON is the shape of the --json output of this command. When --json is passed, if we are not following the
// log stream, we print an array of logEntry objects. If we are following the log stream, we instead print each object
// at top level.
type logEntryJSON struct {
ID string
Timestamp string
Message string
}