Beispiel #1
0
func parseApp(args []string) ([]string, *schema.RuntimeApp, error) {
	if len(args) == 0 {
		return nil, nil, nil
	}

	rtapp := schema.RuntimeApp{}

	// Parse first argument (image name)
	if h, err := types.NewHash(args[0]); err == nil {
		rtapp.Image.ID = *h
		rtapp.Name.Set(h.String()) // won't err
	} else if dapp, err := discovery.NewAppFromString(args[0]); err == nil {
		rtapp.Image.Name = &dapp.Name
		rtapp.Name.Set(path.Base(dapp.Name.String())) // won't err here
		if ll, err := types.LabelsFromMap(dapp.Labels); err != nil {
			return args, nil, err
		} else {
			rtapp.Image.Labels = ll
		}
	} else {
		return args, nil, err
	}

	fl := flag.NewFlagSet(args[0], flag.ExitOnError)
	fl.Var(&rtapp.Name, "name", "App name")
	fl.Var((*AnnotationsFlag)(&rtapp.Annotations), "a", "Add annotation (NAME=VALUE)")
	fl.Var((*MountsFlag)(&rtapp.Mounts), "m", "Mount volume (VOLUME[:MOUNTPOINT])")
	// TODO: app override
	fl.Parse(args[1:])
	return fl.Args(), &rtapp, nil
}
Beispiel #2
0
func runDiscover(args []string) (exit int) {
	if len(args) < 1 {
		stderr("discover: at least one name required")
	}

	for _, name := range args {
		app, err := discovery.NewAppFromString(name)
		if app.Labels["os"] == "" {
			app.Labels["os"] = runtime.GOOS
		}
		if app.Labels["arch"] == "" {
			app.Labels["arch"] = runtime.GOARCH
		}
		if err != nil {
			stderr("%s: %s", name, err)
			return 1
		}
		eps, attempts, err := discovery.DiscoverEndpoints(*app, transportFlags.Insecure)
		if err != nil {
			stderr("error fetching %s: %s", name, err)
			return 1
		}
		for _, a := range attempts {
			fmt.Printf("discover walk: prefix: %s error: %v\n", a.Prefix, a.Error)
		}
		for _, aciEndpoint := range eps.ACIEndpoints {
			fmt.Printf("ACI: %s, ASC: %s\n", aciEndpoint.ACI, aciEndpoint.ASC)
		}
		if len(eps.Keys) > 0 {
			fmt.Println("Keys: " + strings.Join(eps.Keys, ","))
		}
	}

	return
}
Beispiel #3
0
func (n ACFullname) LatestVersion() (string, error) {
	app, err := discovery.NewAppFromString(n.Name() + ":latest")
	if app.Labels["os"] == "" {
		app.Labels["os"] = "linux"
	}
	if app.Labels["arch"] == "" {
		app.Labels["arch"] = "amd64"
	}

	endpoint, _, err := discovery.DiscoverEndpoints(*app, nil, false)
	if err != nil {
		return "", errors.Annotate(err, "Latest discovery fail")
	}

	r, _ := regexp.Compile(`^(\d+\.)?(\d+\.)?(\*|\d+)(\-[\dA-Za-z]+){0,1}$`)

	url := getRedirectForLatest(endpoint.ACIEndpoints[0].ACI)
	logs.WithField("url", url).Debug("latest verion url")

	for _, part := range strings.Split(url, "/") {
		if r.Match([]byte(part)) {
			return part, nil
		}
	}
	return "", errors.New("No latest version found")
}
Beispiel #4
0
// metaDiscoverPubKeyLocations discovers the locations of public keys through ACDiscovery by applying prefix as an ACApp
func (m *Manager) metaDiscoverPubKeyLocations(prefix string) ([]string, error) {
	app, err := discovery.NewAppFromString(prefix)
	if err != nil {
		return nil, err
	}

	hostHeaders := config.ResolveAuthPerHost(m.AuthPerHost)
	insecure := discovery.InsecureNone
	if m.InsecureAllowHTTP {
		insecure = insecure | discovery.InsecureHttp
	}
	if m.InsecureSkipTLSCheck {
		insecure = insecure | discovery.InsecureTls
	}
	ep, attempts, err := discovery.DiscoverPublicKeys(*app, hostHeaders, insecure)
	if err != nil {
		return nil, err
	}

	if m.Debug {
		for _, a := range attempts {
			log.PrintE(fmt.Sprintf("meta tag 'ac-discovery-pubkeys' not found on %s", a.Prefix), a.Error)
		}
	}

	return ep.Keys, nil
}
Beispiel #5
0
func (n ACFullname) LatestVersion() (string, error) {
	app, err := discovery.NewAppFromString(n.Name() + ":latest")
	if app.Labels["os"] == "" {
		app.Labels["os"] = "linux"
	}
	if app.Labels["arch"] == "" {
		app.Labels["arch"] = "amd64"
	}

	endpoints, _, err := discovery.DiscoverACIEndpoints(*app, nil, discovery.InsecureTLS|discovery.InsecureHTTP) //TODO support security
	if err != nil {
		return "", errors.Annotate(err, "Latest discovery fail")
	}

	r, _ := regexp.Compile(`^\d+(.\d+){0,2}(-[\.\-\dA-Za-z]+){0,1}$`) // TODO this is nexus specific

	if len(endpoints) == 0 {
		return "", errs.WithF(data.WithField("aci", string(n)), "Discovery does not give an endpoint to check latest version")
	}

	url := getRedirectForLatest(endpoints[0].ACI)
	logs.WithField("url", url).Debug("latest verion url")

	for _, part := range strings.Split(url, "/") {
		if r.Match([]byte(part)) {
			return part, nil
		}
	}
	return "", errors.New("No latest version found")
}
Beispiel #6
0
func TestNewAppcFromAppString(t *testing.T) {
	tests := []struct {
		appcRef  string
		expected string
	}{
		{
			"example.com/app01",
			"cimd:appc:v=0:example.com/app01",
		},
		{
			"example.com/app01:v1.0.0",
			"cimd:appc:v=0:example.com/app01?version=v1.0.0",
		},
		{
			"example.com/app01,version=v1.0.0",
			"cimd:appc:v=0:example.com/app01?version=v1.0.0",
		},
		{
			"example.com/app01,version=v1.0.0,label01=?&*/",
			"cimd:appc:v=0:example.com/app01?label01=%3F%26%2A%2F&version=v1.0.0",
		},
		{
			"example-app01",
			"cimd:appc:v=0:example-app01",
		},
		{
			"example-app01:v1.0",
			"cimd:appc:v=0:example-app01?version=v1.0",
		},
	}

	for _, tt := range tests {
		app, err := discovery.NewAppFromString(tt.appcRef)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		appc := NewAppcFromApp(app)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}

		u, err := url.Parse(tt.expected)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		td, err := NewAppc(u)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if !appc.Equals(td) {
			t.Errorf("expected identical distribution but got %q != %q", td.CIMD().String(), appc.CIMD().String())
			continue
		}
	}
}
Beispiel #7
0
func TestApp(t *testing.T) {
	tests := []struct {
		uriStr string
		out    string
	}{
		{
			"cimd:appc:v=0:example.com/app01",
			"example.com/app01",
		},
		{
			"cimd:appc:v=0:example.com/app01?version=v1.0.0",
			"example.com/app01:v1.0.0",
		},
		{
			"cimd:appc:v=0:example.com/app01?version=v1.0.0",
			"example.com/app01,version=v1.0.0",
		},
		{
			"cimd:appc:v=0:example.com/app01?label01=%3F%26%2A%2F&version=v1.0.0",
			"example.com/app01,version=v1.0.0,label01=?&*/",
		},
		{
			"cimd:appc:v=0:example-app01",
			"example-app01",
		},
		{
			"cimd:appc:v=0:example-app01?version=v1.0",
			"example-app01:v1.0",
		},
	}

	for _, tt := range tests {
		u, err := url.Parse(tt.uriStr)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		appc, err := NewAppc(u)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}

		app := appc.(*Appc).App()

		expectedApp, err := discovery.NewAppFromString(tt.out)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if !reflect.DeepEqual(app, expectedApp) {
			t.Fatalf("expected app %s, but got %q", expectedApp.String(), app.String())
		}
	}
}
Beispiel #8
0
// RetrieveImage can be used to retrieve a remote image, and optionally discover
// an image based on the App Container Image Discovery specification. Supports
// handling local images as well as
func RetrieveImage(imageUri string, insecure bool) (ReaderCloserSeeker, error) {
	u, err := url.Parse(imageUri)
	if err != nil {
		return nil, err
	}

	switch u.Scheme {
	case "file":
		// for file:// urls, just load the file and return it
		return os.Open(u.Path)

	case "http", "https":
		// Handle HTTP retrievals, wrapped with a tempfile that cleans up.
		resp, err := Client.Get(imageUri)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

		switch resp.StatusCode {
		case http.StatusOK:
		default:
			return nil, fmt.Errorf("HTTP %d on retrieving %q", resp.StatusCode, imageUri)
		}

		return newTempReader(resp.Body)

	case "":
		app, err := discovery.NewAppFromString(imageUri)
		if err != nil {
			return nil, err
		}

		endpoints, _, err := discovery.DiscoverEndpoints(*app, insecure)
		if err != nil {
			return nil, err
		}

		for _, ep := range endpoints.ACIEndpoints {
			r, err := RetrieveImage(ep.ACI, insecure)
			if err != nil {
				continue
			}
			// FIXME should also attempt to validate the signature
			return r, nil
		}
		return nil, fmt.Errorf("failed to find a valid image for %q", imageUri)

	default:
		return nil, fmt.Errorf("%q scheme not supported", u.Scheme)
	}
}
Beispiel #9
0
func runAddDep(cmd *cobra.Command, args []string) (exit int) {
	if len(args) == 0 {
		cmd.Usage()
		return 1
	}
	if len(args) != 1 {
		stderr("dependency add: incorrect number of arguments")
		return 1
	}

	if debug {
		stderr("Adding dependency %q", args[0])
	}

	app, err := discovery.NewAppFromString(args[0])
	if err != nil {
		stderr("dependency add: couldn't parse dependency name: %v", err)
		return 1
	}

	appcLabels := types.Labels(labels)

	for name, value := range app.Labels {
		if _, ok := appcLabels.Get(string(name)); ok {
			stderr("multiple %s labels specified", name)
			return 1
		}
		appcLabels = append(appcLabels, types.Label{
			Name:  name,
			Value: value,
		})
	}

	var hash *types.Hash
	if imageId != "" {
		var err error
		hash, err = types.NewHash(imageId)
		if err != nil {
			stderr("dependency add: couldn't parse image ID: %v", err)
			return 1
		}
	}

	err = newACBuild().AddDependency(app.Name, hash, appcLabels, size)

	if err != nil {
		stderr("dependency add: %v", err)
		return getErrorCode(err)
	}

	return 0
}
Beispiel #10
0
func tryAppFromString(location string) *discovery.App {
	if app, err := discovery.NewAppFromString(location); err != nil {
		return nil
	} else {
		if app.Labels["os"] == "" {
			app.Labels["os"] = runtime.GOOS
		}

		if app.Labels["arch"] == "" {
			app.Labels["arch"] = runtime.GOARCH
		}
		return app
	}
}
Beispiel #11
0
func runDiscover(args []string) (exit int) {
	if len(args) < 1 {
		stderr("discover: at least one name required")
	}

	for _, name := range args {
		app, err := discovery.NewAppFromString(name)
		if app.Labels["os"] == "" {
			app.Labels["os"] = runtime.GOOS
		}
		if app.Labels["arch"] == "" {
			app.Labels["arch"] = runtime.GOARCH
		}
		if err != nil {
			stderr("%s: %s", name, err)
			return 1
		}
		insecure := discovery.InsecureNone
		if transportFlags.Insecure {
			insecure = discovery.InsecureTls | discovery.InsecureHttp
		}
		eps, attempts, err := discovery.DiscoverEndpoints(*app, nil, insecure)
		if err != nil {
			stderr("error fetching %s: %s", name, err)
			return 1
		}
		for _, a := range attempts {
			fmt.Printf("discover walk: prefix: %s error: %v\n", a.Prefix, a.Error)
		}
		if outputJson {
			jsonBytes, err := json.MarshalIndent(&eps, "", "    ")
			if err != nil {
				stderr("error generating JSON: %s", err)
				return 1
			}
			fmt.Println(string(jsonBytes))
		} else {
			for _, aciEndpoint := range eps.ACIEndpoints {
				fmt.Printf("ACI: %s, ASC: %s\n", aciEndpoint.ACI, aciEndpoint.ASC)
			}
			if len(eps.Keys) > 0 {
				fmt.Println("Keys: " + strings.Join(eps.Keys, ","))
			}
		}
	}

	return
}
func (s Service) discoverPod(name cntspec.ACFullname) []cntspec.ACFullname {
	logAci := s.log.WithField("pod", name)

	app, err := discovery.NewAppFromString(name.String())
	if app.Labels["os"] == "" {
		app.Labels["os"] = "linux"
	}
	if app.Labels["arch"] == "" {
		app.Labels["arch"] = "amd64"
	}

	endpoint, _, err := discovery.DiscoverEndpoints(*app, false)
	if err != nil {
		logAci.WithError(err).Fatal("pod discovery failed")
	}

	url := endpoint.ACIEndpoints[0].ACI
	url = strings.Replace(url, "=aci", "=pod", 1) // TODO this is nexus specific

	logUrl := logAci.WithField("url", url)
	response, err := http.Get(url)
	if err != nil {
		logUrl.WithError(err).Fatal("Cannot get pod manifest content")
		return nil
	} else {
		if response.StatusCode != 200 {
			logUrl.WithField("status_code", response.StatusCode).WithField("status_message", response.Status).
				Fatal("Receive response error for discovery")
		}
		defer response.Body.Close()
		content, err := ioutil.ReadAll(response.Body)
		if err != nil {
			logUrl.WithError(err).Fatal("Cannot read pod manifest content")
		}
		tmpMap := make(map[string][]cntspec.ACFullname, 1)
		if err := s.podManifestToMap(tmpMap, content); err != nil {
			logUrl.WithError(err).Fatal("Cannot read pod content")
		}
		acis := tmpMap[name.Name()]
		if acis == nil {
			logUrl.Fatal("Discovered pod name does not match requested")
		}
		return acis
	}
}
Beispiel #13
0
func getStoreKeyFromApp(s *imagestore.Store, img string) (string, error) {
	app, err := discovery.NewAppFromString(img)
	if err != nil {
		return "", errwrap.Wrap(fmt.Errorf("cannot parse the image name %q", img), err)
	}
	labels, err := types.LabelsFromMap(app.Labels)
	if err != nil {
		return "", errwrap.Wrap(fmt.Errorf("invalid labels in the image %q", img), err)
	}
	key, err := s.GetACI(app.Name, labels)
	if err != nil {
		switch err.(type) {
		case imagestore.ACINotFoundError:
			return "", err
		default:
			return "", errwrap.Wrap(fmt.Errorf("cannot find image %q", img), err)
		}
	}
	return key, nil
}
Beispiel #14
0
func parseImageName(name string) (types.ACIdentifier, types.Labels, error) {
	app, err := discovery.NewAppFromString(name)
	if err != nil {
		return "", nil, errors.Trace(err)
	}

	if app.Labels["os"] == "" {
		app.Labels["os"] = runtime.GOOS
	}
	if app.Labels["arch"] == "" {
		app.Labels["arch"] = runtime.GOARCH
	}

	labels, err := types.LabelsFromMap(app.Labels)
	if err != nil {
		return "", nil, errors.Trace(err)
	}

	return app.Name, labels, nil
}
Beispiel #15
0
func newAppBundle(name string) (*appBundle, error) {
	app, err := discovery.NewAppFromString(name)
	if err != nil {
		return nil, errwrap.Wrap(fmt.Errorf("invalid image name %q", name), err)
	}
	if _, ok := app.Labels["arch"]; !ok {
		app.Labels["arch"] = runtime.GOARCH
	}
	if _, ok := app.Labels["os"]; !ok {
		app.Labels["os"] = runtime.GOOS
	}
	if err := types.IsValidOSArch(app.Labels, stage0.ValidOSArch); err != nil {
		return nil, errwrap.Wrap(fmt.Errorf("invalid image name %q", name), err)
	}
	bundle := &appBundle{
		App: app,
		Str: name,
	}
	return bundle, nil
}
Beispiel #16
0
// NewAppc returns an Appc distribution from an Appc distribution URI
func NewAppc(u *url.URL) (Distribution, error) {
	c, err := parseCIMD(u)
	if err != nil {
		return nil, fmt.Errorf("cannot parse URI: %q: %v", u.String(), err)
	}
	if c.Type != TypeAppc {
		return nil, fmt.Errorf("wrong distribution type: %q", c.Type)
	}

	appcStr := c.Data
	for n, v := range u.Query() {
		appcStr += fmt.Sprintf(",%s=%s", n, v[0])
	}
	app, err := discovery.NewAppFromString(appcStr)
	if err != nil {
		return nil, fmt.Errorf("wrong appc image string %q: %v", u.String(), err)
	}

	return NewAppcFromApp(app), nil
}
Beispiel #17
0
func (cnt *Img) processFrom() {
	if cnt.manifest.From != "" {
		log.Get().Info("Prepare rootfs from " + cnt.manifest.From)

		app, err := discovery.NewAppFromString(string(cnt.manifest.From))
		if app.Labels["os"] == "" {
			app.Labels["os"] = "linux"
		}
		if app.Labels["arch"] == "" {
			app.Labels["arch"] = "amd64"
		}

		endpoint, _, err := discovery.DiscoverEndpoints(*app, false)
		if err != nil {
			panic(err)
		}

		url := endpoint.ACIEndpoints[0].ACI

		aciPath := config.GetConfig().AciPath + "/" + string(cnt.manifest.From)
		if _, err := os.Stat(aciPath + "/image.aci"); cnt.args.ForceUpdate || os.IsNotExist(err) {
			if err := os.MkdirAll(aciPath, 0755); err != nil {
				log.Get().Panic(err)
			}
			if err = utils.ExecCmd("wget", "-O", aciPath+"/image.aci", url); err != nil {
				os.Remove(aciPath + "/image.aci")
				log.Get().Panic("Cannot download from image", err)
			}
		} else {
			log.Get().Info("Image " + cnt.manifest.From + " Already exists locally, will not be downloaded")
		}

		utils.ExecCmd("tar", "xpf", aciPath+"/image.aci", "-C", cnt.target)

		//		utils.ExecCmd("rkt", "--insecure-skip-verify=true", "fetch", cnt.manifest.From)
		//		utils.ExecCmd("rkt", "image", "export", "--overwrite", cnt.manifest.From, cnt.target + "/from.aci")
		//		utils.ExecCmd("tar", "xf", cnt.target + "/from.aci", "-C", cnt.target)
		//		os.Remove(cnt.target + "/from.aci")
	}
}
Beispiel #18
0
func (a *ACBuild) beginFromRemoteImage(start string, insecure bool) error {
	app, err := discovery.NewAppFromString(start)
	if err != nil {
		return err
	}
	labels, err := types.LabelsFromMap(app.Labels)
	if err != nil {
		return err
	}

	tmpDepStoreTarPath, err := ioutil.TempDir("", "acbuild-begin-tar")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tmpDepStoreTarPath)

	tmpDepStoreExpandedPath, err := ioutil.TempDir("", "acbuild-begin-expanded")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tmpDepStoreExpandedPath)

	reg := registry.Registry{
		DepStoreTarPath:      tmpDepStoreTarPath,
		DepStoreExpandedPath: tmpDepStoreExpandedPath,
		Insecure:             insecure,
		Debug:                a.Debug,
	}

	err = reg.Fetch(app.Name, labels, 0, false)
	if err != nil {
		if urlerr, ok := err.(*url.Error); ok {
			if operr, ok := urlerr.Err.(*net.OpError); ok {
				if dnserr, ok := operr.Err.(*net.DNSError); ok {
					if dnserr.Err == "no such host" {
						return fmt.Errorf("unknown host when fetching image, check your connection and local file paths must start with '/' or '.'")
					}
				}
			}
		}
		return err
	}

	files, err := ioutil.ReadDir(tmpDepStoreTarPath)
	if err != nil {
		return err
	}

	if len(files) != 1 {
		var filelist string
		for _, file := range files {
			if filelist == "" {
				filelist = file.Name()
			} else {
				filelist = filelist + ", " + file.Name()
			}
		}
		panic("unexpected number of files in store after download: " + filelist)
	}

	return util.ExtractImage(path.Join(tmpDepStoreTarPath, files[0].Name()), a.CurrentACIPath, nil)
}
Beispiel #19
0
// DistFromImageString return the distribution for the given input image string
func DistFromImageString(is string) (dist.Distribution, error) {
	u, err := url.Parse(is)
	if err != nil {
		return nil, errwrap.Wrap(fmt.Errorf("failed to parse image url %q", is), err)
	}

	// Convert user friendly image string names to internal distribution URIs
	// file:///full/path/to/aci/file.aci -> archive:aci:file%3A%2F%2F%2Ffull%2Fpath%2Fto%2Faci%2Ffile.aci
	switch u.Scheme {
	case "":
		// no scheme given, hence it is an appc image name or path
		appImageType := guessAppcOrPath(is, []string{schema.ACIExtension})

		switch appImageType {
		case imageStringName:
			app, err := discovery.NewAppFromString(is)
			if err != nil {
				return nil, fmt.Errorf("invalid appc image string %q: %v", is, err)
			}
			return dist.NewAppcFromApp(app), nil
		case imageStringPath:
			absPath, err := filepath.Abs(is)
			if err != nil {
				return nil, errwrap.Wrap(fmt.Errorf("failed to get an absolute path for %q", is), err)
			}
			is = "file://" + absPath

			// given a file:// image string, call this function again to return an ACI distribution
			return DistFromImageString(is)
		default:
			return nil, fmt.Errorf("invalid image string type %q", appImageType)
		}
	case "file", "http", "https":
		// An ACI archive with any transport type (file, http, s3 etc...) and final aci extension
		if filepath.Ext(u.Path) == schema.ACIExtension {
			dist, err := dist.NewACIArchiveFromTransportURL(u)
			if err != nil {
				return nil, fmt.Errorf("archive distribution creation error: %v", err)
			}
			return dist, nil
		}
	case "docker":
		// Accept both docker: and docker:// uri
		dockerStr := is
		if strings.HasPrefix(dockerStr, "docker://") {
			dockerStr = strings.TrimPrefix(dockerStr, "docker://")
		} else if strings.HasPrefix(dockerStr, "docker:") {
			dockerStr = strings.TrimPrefix(dockerStr, "docker:")
		}

		dist, err := dist.NewDockerFromString(dockerStr)
		if err != nil {
			return nil, fmt.Errorf("docker distribution creation error: %v", err)
		}
		return dist, nil
	case dist.Scheme: // cimd
		return dist.Parse(is)
	default:
		// any other scheme is a an appc image name, i.e. "my-app:v1.0"
		app, err := discovery.NewAppFromString(is)
		if err != nil {
			return nil, fmt.Errorf("invalid appc image string %q: %v", is, err)
		}

		return dist.NewAppcFromApp(app), nil
	}

	return nil, fmt.Errorf("invalid image string %q", is)
}
Beispiel #20
0
func runDiscover(args []string) (exit int) {
	if len(args) < 1 {
		stderr("discover: at least one name required")
	}

	for _, name := range args {
		app, err := discovery.NewAppFromString(name)
		if app.Labels["os"] == "" {
			app.Labels["os"] = runtime.GOOS
		}
		if app.Labels["arch"] == "" {
			app.Labels["arch"] = runtime.GOARCH
		}
		if err != nil {
			stderr("%s: %s", name, err)
			return 1
		}
		insecure := discovery.InsecureNone
		if transportFlags.Insecure {
			insecure = discovery.InsecureTLS | discovery.InsecureHTTP
		}
		eps, attempts, err := discovery.DiscoverACIEndpoints(*app, nil, insecure)
		if err != nil {
			stderr("error fetching endpoints for %s: %s", name, err)
			return 1
		}
		for _, a := range attempts {
			fmt.Printf("discover endpoints walk: prefix: %s error: %v\n", a.Prefix, a.Error)
		}
		publicKeys, attempts, err := discovery.DiscoverPublicKeys(*app, nil, insecure)
		if err != nil {
			stderr("error fetching public keys for %s: %s", name, err)
			return 1
		}
		for _, a := range attempts {
			fmt.Printf("discover public keys walk: prefix: %s error: %v\n", a.Prefix, a.Error)
		}

		type discoveryData struct {
			ACIEndpoints []discovery.ACIEndpoint
			PublicKeys   []string
		}

		if outputJson {
			dd := discoveryData{ACIEndpoints: eps, PublicKeys: publicKeys}
			jsonBytes, err := json.MarshalIndent(dd, "", "    ")
			if err != nil {
				stderr("error generating JSON: %s", err)
				return 1
			}
			fmt.Println(string(jsonBytes))
		} else {
			for _, aciEndpoint := range eps {
				fmt.Printf("ACI: %s, ASC: %s\n", aciEndpoint.ACI, aciEndpoint.ASC)
			}
			if len(publicKeys) > 0 {
				fmt.Println("PublicKeys: " + strings.Join(publicKeys, ","))
			}
		}
	}

	return
}