Compare commits

..

11 Commits

Author SHA1 Message Date
Jeff McCune
490f91f580 cli: hide unsupported commands (#289)
Use a simple feature flag system that checks env vars if a feature is
enabled.
2024-10-31 10:04:01 -07:00
Jeff McCune
79b065cda8 website: add open graph image for helm guide try 6 2024-10-30 12:23:18 -07:00
Jeff McCune
0fa6047552 website: add open graph image for helm guide try 5 2024-10-30 12:20:19 -07:00
Jeff McCune
11ecc0cc3a website: add open graph image for helm guide try 4 2024-10-30 12:04:39 -07:00
Jeff McCune
a62e4ba117 website: add open graph image for helm guide try 3 2024-10-30 11:56:01 -07:00
Jeff McCune
07fe667f30 website: add open graph image for helm guide try 2 2024-10-30 11:40:39 -07:00
Jeff McCune
3ad994cbb9 website: add open graph image for helm guide 2024-10-30 11:26:54 -07:00
Jeff McCune
b3d9bd32af website: add why cue for configuration blog post
This is going to be one of the first questions we get.
2024-10-28 21:30:11 -07:00
Jeff McCune
d398b49d7f website: fix head title tag try 2
The open graph title was still showing up poorly, docusaurus generates
it with the Holos | Holos repetition, so we need to override it.
2024-10-28 14:37:43 -07:00
Jeff McCune
12179a6991 website: fix head title tag and social card
Generate the social card manually from https://www.opengraph.xyz/
Override the page title tag, otherwise it shows up as "Announcing Holos
| Holos" in social links, which is weird.
2024-10-28 14:07:40 -07:00
Jeff McCune
fee472bb66 website: add stock social card for annoucement 2024-10-28 13:31:10 -07:00
30 changed files with 227 additions and 49 deletions

View File

@@ -53,6 +53,7 @@
"CODEOWNERS",
"configmap",
"configmapargs",
"connectrpc",
"cookiesecret",
"coredns",
"corev",
@@ -77,6 +78,7 @@
"entgo",
"envoyfilter",
"envoyfilters",
"errdetails",
"errgroup",
"etcdsnapshotfiles",
"externalsecret",
@@ -93,6 +95,7 @@
"generationbehavior",
"generatorargs",
"generatoroptions",
"genproto",
"ggnpl",
"ghaction",
"githubaccesstokens",
@@ -100,6 +103,7 @@
"godoc",
"golangci",
"gomarkdoc",
"googleapis",
"goreleaser",
"gotypesalias",
"grpcreflect",
@@ -157,6 +161,7 @@
"logfmt",
"mattn",
"mccutchen",
"metav",
"mindmap",
"mktemp",
"msqbn",

View File

@@ -1,9 +1,15 @@
---
description: Learn how Holos adds valuable features to Helm and Kustomize.
slug: /guides/helm
title: Helm
description: Learn how Holos implements the rendered manifest pattern with Helm, Kustomize, and CUE.
sidebar_position: 150
---
<head>
<meta property="og:title" content="Helm Guide | Holos" />
<meta property="og:image" content="https://holos.run/img/cards/guides-helm-2.png" />
</head>
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';

View File

@@ -3,9 +3,16 @@ slug: announcing-holos
title: Announcing Holos
authors: [jeff]
tags: [holos, launch]
image: /img/cards/announcing-holos.png
description: Holistically manage Helm and Kustomize with CUE
---
Im excited to share Holos, a Go command line tool we developed to make it
<head>
<title>Announcing Holos</title>
<meta property="og:title" content="Announcing Holos" />
</head>
I'm excited to share Holos, a Go command line tool we developed to make it
easier to manage a platform built on Kubernetes. Holos implements the rendered
manifests pattern as a data pipeline to fully render manifests generated from
[Helm], [Kustomize], or [CUE] in a holistic way.

View File

@@ -0,0 +1,123 @@
---
slug: why-cue-for-configuration
title: Why CUE for Configuration
authors: [jeff]
tags: [holos, cue]
image: /img/cards/why-cue.png
description: Why we use CUE for configuration in Holos
date: 2024-10-28T16:00
---
We selected [CUE](https://cuelang.org/) as the configuration language in Holos
for a number of reasons described in this post. The process was a combination
of process by elimination and the unique way CUE _unifies_ configuration.
<!-- truncate -->
We evaluated a number of domain specific and general purpose languages before
deciding on CUE. The CUE website, GitHub issues, and Marcel's videos do a great
job of explaining most of these reasons, so I'll summarize and cite them here.
## DSL or GPL
The first decision was if we should use a turing complete general purpose
language, or a domain specific language (DSL). We decided to use a DSL because
we knew from hard won experience configuration with general purpose languages
invites too many problems over time.
1. Configuration easily becomes non-deterministic, especially when remote procedure calls are involved.
2. Many general purpose languages support type checking, but few support constraints and validation of data. We must write our own validation logic which often means validation happens haphazardly, if at all.
3. Data is usually mutable, making it difficult to know where an output value came from.
4. Configuration code is read much more frequently, and at more critical times like an outage, than it's written. I felt this pain and I don't want anyone using Holos to feel that way.
For these reasons we sought a domain specific language that focused on
simplicity, readability, and data validation. This quote from Marcel got my attention focused on CUE.
> I would argue that for configuration languages maintainability and readability are more important even than for programming languages, because they are ofter viewed by a larger group, often need to be changed in emergency conditions, and also as they are supposed to convey a certain contract. Most configuration languages, like GCL (my own doing), are more like scripting languages, making it easier to crank out definitions of large swats of data compactly, but being harder to comprehend and modify later.
Source: [Comparisons between CUE, Jsonnet, Shall, OPA, etc.](https://github.com/cuelang/cue/discussions/669#discussioncomment-306811)
## Other DSLs
### Template Engines
Template engines are not exactly a domain specific language, but they're
similar. We already used Go templates in Helm to produce YAML, and previously
used Jinja2 and ERB templates extensively for configuration tasks.
The fundamental problem with text template engines is that they manipulate text,
not data. As a result, output is often rendered without error or indication the
configuration is invalid until it is applied to the live system. Errors need
to be handled faster and earlier, ideally immediately as we're writing in our
editor.
For these reasons we can set aside all tools based on text templating.
### Jsonnet
Marcel and the CUE website explain this much better than I can. We used Jsonnet
to configure the kubernetes prometheus stack and experienced Jsonnet's lack of
validation features first hand.
> Like Jsonnet, CUE is a superset of JSON. They also are both influenced by GCL. CUE, in turn is influenced by Jsonnet. This may give the semblance that the languages are very similar. At the core, though, they are very different.
>
> CUEs focus is data validation whereas Jsonnet focuses on data templating (boilerplate removal). Jsonnet was not designed with validation in mind.
>
> Jsonnet and GCL can be quite powerful at reducing boilerplate. The goal of CUE is not to be better at boilerplate removal than Jsonnet or GCL. CUE was designed to be an answer to two major shortcomings of these approaches: complexity and lack of typing. Jsonnet reduces some of the complexities of GCL, but largely falls into the same category. For CUE, the tradeoff was to add typing and reduce complexity (for humans and machines), at the expense of giving up flexibility.
Source: [CUE Configuration Use Case - Jsonnet / GCL](https://cuelang.org/docs/concept/configuration-use-case/#jsonnet-gcl)
Marcel answered this question in more depth earlier:
> Jsonnet is based on BCL, an internal language at Google. It fixes a few things relative to BCL, but is mostly the same. This means it copies the biggest mistakes of BCL. Even though BCL is still widely used at Google, its issues are clear. It was just that the alternatives weren't that much better.
>
> There are a myriad of issues with BCL (and Jsonnet and pretty much all of its descendants), but I will mention a couple:
>
> 1. Most notably, the basic operation of composition of BCL/Jsonnet, inheritance, is not commutative and idempotent in the general case. In other words, order matters. This makes it, for humans, hard to track where values are coming from. But also, it makes it very complicated, if not impossible, to do any kind of automation. The complexity of inheritance is compounded by the fact that values can enter an object from one of several directions (super, overlay, etc.), and the order in which this happens matters. The basic operation of CUE is commutative, associative and idempotent. This order independence helps both humans and machines. The resulting model is much less complex.
> 2. Typing: most of the BCL offshoots do not allow for schema definitions. This makes it hard to detect any kind of typos or user errors. For a large code bases, no one will question a requirement to have a compiled/typed language. Why should we not require the same kind of rigor for data? Some offshoots of BCL internal to Google and also external have tried to address this a bit, but none quite satisfactory. In CUE types and values are the same thing. This makes things both easier than schema-based languages (less concepts to learn), but also more powerful. It allows for intuitive but also precise typing.
>
> There are many other issues, like handling cycles, unprincipled workarounds for hermeticity, poor tooling and so forth that make BCL and offsprings often awkward.
>
> So why CUE? Configuration is still largely an unsolved problem. We have tried using code to generate configs, or hybrid languages, but that often results in a mess. Using generators on databases doesn't allow keeping it sync with revision control. Simpler approaches like HCL and Kustomize recognize the complexity issue by removing a lot of it, but then sometimes become too weak, and actually also reintroduce some of this complexity with overlays (a poor man's inheritance, if you will, but with some of the same negative consequences). Other forms of removing complexity, for instance by just introducing simpler forms/ abstraction layers of configuration, may work within certain context but are domain-specific and relatively hard to maintain.
>
> So inheritance-based languages, for all its flaws, were the best we had. The idea behind CUE is to recognize that a declarative language is the best approach for many (not all) configuration problems, but to tackle the fundamental issues of these languages.
>
> The idea for CUE is actually not new. It was invented about 30 years ago and has been in use and further developed since that time in the field of computational linguistics, where the concept is used to encode entire lexicons as well as very detailed grammars of human languages. If you think about it, these are huge configurations that are often maintained by both computer scientists and linguists. You can see this as a proof of concept that large-scale, declarative configuration for a highly complex domain can work.
>
> CUE is a bit different from the languages used in linguistics and more tailored to the general configuration issue as we've seen it at Google. But under the hood it adheres strictly to the concepts and principles of these approaches and we have been careful not to make the same mistakes made in BCL (which then were copied in all its offshoots). It also means that CUE can benefit from 30 years of research on this topic. For instance, under the hood, CUE uses a first-order unification algorithm, allowing us to build template extractors based on anti-unification (see issue #7 and #15), something that is not very meaningful or even possible with languages like BCL and Jsonnet.
Source: [how CUE differs from jsonnet](https://github.com/cuelang/cue/issues/33#issuecomment-483615374)
### Dhall
> Dhall addresses some of the issues of GCL and Jsonnet (like lack of typing), but lacks the detailed typing of CUE. But it still misses the most important property of CUE: its model of composability. Some of the benefits are explained in the above link. Conceptually, CUE is an aspect-oriented and constraint-based language. It allows you to specify fine-grained constraints on what are valid values. These constraints then double as templates, allowing to remove boilerplate often with the same efficacy as inheritance, even if it works very differently.
Source [Comparisons between CUE, Jsonnet, Dhall, OPA, etc.](https://github.com/cuelang/cue/discussions/669#discussioncomment-306811)
### Rego (OPA)
> CUE also can be used for policy specification, like Rego (OPA).CUE unifies values, types, and constraints in a single continuum. As it is a constraint-based language first and foremost, it is well suited for defining policy. It is less developed in that area than Rego, but it I expect it will ultimately be better suited for policy. Note that Rego is based on Datalog, which is more of a query language at hart, giving it quite a different feel for defining policy than CUE. Both are logic programming languages, though, and share many of the same properties.
Source [Comparisons between CUE, Jsonnet, Dhall, OPA, etc.](https://github.com/cuelang/cue/discussions/669#discussioncomment-306811)
### PKL
I didn't look deeply into [Pkl](https://github.com/apple/pkl) primarily because
CUE, like Holos, is written in Go. It was straight forward to integrate CUE
into Holos.
### HCL
I have extensive experience with HCL and found it challenging to work with at medium to large scales.
See also: [CUE Configuration Use Case - HCL](https://cuelang.org/docs/concept/configuration-use-case/#hcl)
## Editor Integration
CUE has good support today for Visual Studio Code, and better support coming,
see the [CUE LSP Roadmap](https://github.com/orgs/cue-lang/projects/15)
## Additional Resources
The video [Large-Scale Engineering of Configuration with Unification (Marcel van
Lohuizen)](https://www.youtube.com/watch?v=jSRXobu1jHk) motivated me to go
deeper and invest significant time into CUE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

View File

@@ -43,8 +43,9 @@ func makeBuildRunFunc(cfg *client.Config) command.RunFunc {
}
// New returns the build subcommand for the root command
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("build DIRECTORY")
cmd.Hidden = !feature.Flag(holos.BuildFeature)
cmd.Args = cobra.ExactArgs(1)
cmd.Short = "write kubernetes manifests to standard output"
cmd.Example = " holos build components/argo/crds"

View File

@@ -12,9 +12,10 @@ import (
)
// New returns the create command for the cli
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("create")
cmd.Short = "create resources"
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Flags().SortFlags = false
cmd.RunE = func(c *cobra.Command, args []string) error {
return c.Usage()

View File

@@ -11,8 +11,9 @@ import (
)
// New returns the command for the cli
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("delete")
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Aliases = []string{"destroy"}
cmd.Short = "delete resources"
cmd.Flags().SortFlags = false

View File

@@ -14,14 +14,14 @@ import (
)
// New returns a new generate command.
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("generate")
cmd.Aliases = []string{"gen"}
cmd.Short = "generate local resources"
cmd.Args = cobra.NoArgs
cmd.AddCommand(NewPlatform(cfg))
cmd.AddCommand(NewComponent())
cmd.AddCommand(NewComponent(feature))
return cmd
}
@@ -48,9 +48,10 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
}
// NewComponent returns a command to generate a holos component
func NewComponent() *cobra.Command {
func NewComponent(feature holos.Flagger) *cobra.Command {
cmd := command.New("component")
cmd.Short = "generate a component from an embedded schematic"
cmd.Hidden = !feature.Flag(holos.GenerateComponentFeature)
for _, name := range generate.Components("v1alpha3") {
cmd.AddCommand(makeSchematicCommand("v1alpha3", name))

View File

@@ -16,8 +16,10 @@ import (
)
// New returns the get command for the cli.
func New(hc *holos.Config) *cobra.Command {
func New(hc *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("get")
// not supported as of v0.97
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Short = "get resources"
cmd.Aliases = []string{"list"}
cmd.Flags().SortFlags = false

View File

@@ -10,8 +10,9 @@ import (
)
// New returns the kv root command for the cli
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("kv")
cmd.Hidden = !feature.Flag(holos.SecretsFeature)
cmd.Short = "work with secrets in the provisioner cluster"
cmd.Flags().SortFlags = false
cmd.RunE = func(c *cobra.Command, args []string) error {

View File

@@ -13,8 +13,9 @@ import (
)
// New returns a new login command.
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("login")
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Short = "log in by caching credentials"
var printClaims bool

View File

@@ -11,8 +11,9 @@ import (
"github.com/spf13/cobra"
)
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("logout")
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Short = "log out by deleting cached credentials"
cmd.RunE = func(c *cobra.Command, args []string) error {
if err := os.RemoveAll(token.CacheDir); err != nil {

View File

@@ -18,7 +18,8 @@ func MakeMain(options ...holos.Option) func() int {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if err := New(cfg).ExecuteContext(ctx); err != nil {
feature := &holos.EnvFlagger{}
if err := New(cfg, feature).ExecuteContext(ctx); err != nil {
return HandleError(ctx, err, cfg)
}
return 0

View File

@@ -25,10 +25,10 @@ func newConfig() (*config, *flag.FlagSet) {
}
// New returns the preflight command for the root command.
func New(hc *holos.Config) *cobra.Command {
func New(hc *holos.Config, feature holos.Flagger) *cobra.Command {
cfg, flagSet := newConfig()
cmd := command.New("preflight")
cmd.Hidden = !feature.Flag(holos.PreflightFeature)
cmd.Short = "run holos preflight checks"
cmd.Flags().AddGoFlagSet(flagSet)
cmd.RunE = makePreflightRunFunc(hc, cfg)

View File

@@ -14,8 +14,9 @@ import (
"github.com/spf13/cobra"
)
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("pull")
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Short = "pull resources from holos server"
cmd.Args = cobra.NoArgs

View File

@@ -14,9 +14,10 @@ import (
"github.com/spf13/cobra"
)
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("push")
cmd.Short = "push resources to holos server"
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Args = cobra.NoArgs
config := client.NewConfig(cfg)

View File

@@ -10,8 +10,9 @@ import (
)
// New returns a new register command.
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("register")
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Short = "rpc UserService.RegisterUser"
cmd.Long = "register with holos server"
cmd.Args = cobra.NoArgs

View File

@@ -22,10 +22,10 @@ import (
"github.com/spf13/cobra"
)
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("render")
cmd.Args = cobra.NoArgs
cmd.Short = "render platforms and components into the deploy/ directory"
cmd.Short = "render platforms and components to manifest files"
cmd.AddCommand(NewComponent(cfg))
cmd.AddCommand(NewPlatform(cfg))
return cmd
@@ -35,7 +35,7 @@ func New(cfg *holos.Config) *cobra.Command {
func NewComponent(cfg *holos.Config) *cobra.Command {
cmd := command.New("component DIRECTORY")
cmd.Args = cobra.ExactArgs(1)
cmd.Short = "render specific components"
cmd.Short = "render a platform component"
cmd.Example = " holos render component --inject holos_cluster=aws2 ./components/monitoring/kube-prometheus-stack"
cmd.Flags().AddGoFlagSet(cfg.WriteFlagSet())
cmd.Flags().AddGoFlagSet(cfg.ClusterFlagSet())

View File

@@ -35,7 +35,7 @@ import (
var helpLong string
// New returns a new root *cobra.Command for command line execution.
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
rootCmd := &cobra.Command{
Use: "holos",
Short: "holos manages a holistic integrated software development platform",
@@ -67,36 +67,37 @@ func New(cfg *holos.Config) *cobra.Command {
rootCmd.PersistentFlags().AddGoFlagSet(cfg.LogFlagSet())
// subcommands
rootCmd.AddCommand(build.New(cfg))
rootCmd.AddCommand(render.New(cfg))
rootCmd.AddCommand(get.New(cfg))
rootCmd.AddCommand(create.New(cfg))
rootCmd.AddCommand(destroy.New(cfg))
rootCmd.AddCommand(preflight.New(cfg))
rootCmd.AddCommand(login.New(cfg))
rootCmd.AddCommand(logout.New(cfg))
rootCmd.AddCommand(token.New(cfg))
rootCmd.AddCommand(generate.New(cfg))
rootCmd.AddCommand(register.New(cfg))
rootCmd.AddCommand(pull.New(cfg))
rootCmd.AddCommand(push.New(cfg))
rootCmd.AddCommand(newOrgCmd())
rootCmd.AddCommand(build.New(cfg, feature))
rootCmd.AddCommand(render.New(cfg, feature))
rootCmd.AddCommand(get.New(cfg, feature))
rootCmd.AddCommand(create.New(cfg, feature))
rootCmd.AddCommand(destroy.New(cfg, feature))
rootCmd.AddCommand(preflight.New(cfg, feature))
rootCmd.AddCommand(login.New(cfg, feature))
rootCmd.AddCommand(logout.New(cfg, feature))
rootCmd.AddCommand(token.New(cfg, feature))
rootCmd.AddCommand(generate.New(cfg, feature))
rootCmd.AddCommand(register.New(cfg, feature))
rootCmd.AddCommand(pull.New(cfg, feature))
rootCmd.AddCommand(push.New(cfg, feature))
rootCmd.AddCommand(newOrgCmd(feature))
// Maybe not needed?
rootCmd.AddCommand(txtar.New(cfg))
// Deprecated, remove?
rootCmd.AddCommand(kv.New(cfg))
rootCmd.AddCommand(kv.New(cfg, feature))
// Server
rootCmd.AddCommand(server.New(cfg))
rootCmd.AddCommand(server.New(cfg, feature))
return rootCmd
}
func newOrgCmd() (cmd *cobra.Command) {
func newOrgCmd(feature holos.Flagger) (cmd *cobra.Command) {
cmd = command.New("orgid")
cmd.Short = "print the current context org id."
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
cc := holos.NewClientContext(ctx)

View File

@@ -14,7 +14,7 @@ import (
func newCommand() (*cobra.Command, *bytes.Buffer) {
var b1, b2 bytes.Buffer
// discard stdout for now, it's a bunch of usage messages.
cmd := New(holos.New(holos.Stdout(&b1), holos.Stderr(&b2)))
cmd := New(holos.New(holos.Stdout(&b1), holos.Stderr(&b2)), &holos.EnvFlagger{})
return cmd, &b2
}
@@ -90,7 +90,7 @@ func TestInvalidArgs(t *testing.T) {
}
for _, args := range invalidArgs {
var b bytes.Buffer
cmd := New(holos.New(holos.Stdout(&b)))
cmd := New(holos.New(holos.Stdout(&b)), &holos.EnvFlagger{})
cmd.SetArgs(args)
err := cmd.Execute()
if err == nil {
@@ -115,7 +115,7 @@ func TestLoggerFromContext(t *testing.T) {
func TestVersion(t *testing.T) {
var b bytes.Buffer
cmd := New(holos.New(holos.Stdout(&b)))
cmd := New(holos.New(holos.Stdout(&b)), &holos.EnvFlagger{})
cmd.SetOut(&b)
cmd.SetArgs([]string{"--version"})
if err := cmd.Execute(); err != nil {

View File

@@ -77,7 +77,7 @@ func cmdHolos(ts *testscript.TestScript, neg bool, args []string) {
holos.Stderr(ts.Stderr()),
)
cmd := cli.New(cfg)
cmd := cli.New(cfg, &holos.EnvFlagger{})
cmd.SetArgs(args)
err := cmd.Execute()

View File

@@ -13,8 +13,9 @@ import (
)
// New returns a new login command.
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := command.New("token")
cmd.Hidden = !feature.Flag(holos.ServerFeature)
cmd.Short = "write id token to stdout"
cmd.Long = "Useful with curl / grpcurl -H $(holos token)"

View File

@@ -2,6 +2,7 @@ package holos
import (
"fmt"
"os"
"strings"
)
@@ -25,3 +26,23 @@ func (i *StringSlice) Set(value string) error {
}
return nil
}
type feature string
const BuildFeature = feature("BUILD")
const ServerFeature = feature("SERVER")
const PreflightFeature = feature("PREFLIGHT")
const GenerateComponentFeature = feature("GENERATE_COMPONENT")
const SecretsFeature = feature("SECRETS")
// Flagger is the interface to check if an experimental feature is enabled.
type Flagger interface {
Flag(name feature) bool
}
type EnvFlagger struct{}
func (e *EnvFlagger) Flag(name feature) bool {
envVar := "HOLOS_FEATURE_" + strings.ToUpper(string(name))
return os.Getenv(envVar) != ""
}

View File

@@ -26,11 +26,12 @@ import (
var helpLong string
// New builds a root cobra command with flags linked to the Config field.
func New(cfg *holos.Config) *cobra.Command {
func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd := &cobra.Command{
Use: "server",
Short: "run the holos server",
Long: helpLong,
Use: "server",
Short: "run the holos server",
Hidden: !feature.Flag(holos.ServerFeature),
Long: helpLong,
// We handle our own errors.
SilenceUsage: true,
SilenceErrors: true,

View File

@@ -1 +1 @@
1
2