La semaine dernière, l'équipe d'ingénierie d'OpenSauced a publié Pizza CLI, un outil de ligne de commande puissant et composable pour générer des fichiers CODEOWNER et s'intégrer à la plate-forme OpenSauced. Construire des outils de ligne de commande robustes peut sembler simple, mais sans une planification minutieuse et des paradigmes réfléchis, les CLI peuvent rapidement se transformer en un fouillis de code difficile à maintenir et criblé de bogues. Dans cet article de blog, nous examinerons en profondeur comment nous avons construit cette CLI à l'aide de Go, comment nous organisons nos commandes à l'aide de Cobra et comment notre équipe d'ingénieurs simplifiés itère rapidement pour créer des fonctionnalités puissantes.
La CLI Pizza est un outil de ligne de commande Go qui exploite plusieurs bibliothèques standard. La simplicité, la vitesse et l’accent mis sur la programmation système de Go en font un choix idéal pour créer des CLI. À la base, Pizza-CLI utilise spf13/cobra, une bibliothèque d'amorçage CLI dans Go, pour organiser et gérer l'intégralité de l'arborescence des commandes.
Vous pouvez considérer Cobra comme l'échafaudage qui fait fonctionner une interface de ligne de commande elle-même, permet à tous les indicateurs de fonctionner de manière cohérente et gère la communication avec les utilisateurs via des messages d'aide et une documentation automatisée.
L'un des premiers (et plus grands) défis lors de la création d'une Go CLI basée sur Cobra est de savoir comment structurer tout votre code et vos fichiers. Contrairement à la croyance populaire, il n’existe aucune méthode prescrite pour le faire dans Go. Ni la commande go build ni l'utilitaire gofmt ne se plaindront de la façon dont vous nommez vos packages ou organisez vos répertoires. C'est l'une des meilleures parties de Go : sa simplicité et sa puissance facilitent la définition de structures qui fonctionnent pour vous et votre équipe d'ingénierie !
En fin de compte, à mon avis, il est préférable de penser et de structurer une base de code Go basée sur Cobra comme un arbre de commandes :
├── Root command │ ├── Child command │ ├── Child command │ │ └── Grandchild command
À la base de l'arborescence se trouve la commande racine : c'est l'ancre de l'ensemble de votre application CLI et obtiendra le nom de votre CLI. Attaché en tant que commandes enfants, vous disposerez d'une arborescence de logique de branchement qui informe la structure du fonctionnement de l'ensemble de votre flux CLI.
L'une des choses qu'il est incroyablement facile de manquer lors de la création de CLI est l'expérience utilisateur. Je recommande généralement aux gens de suivre un paradigme de « nom de verbe racine » lors de la création de commandes et de structures de commandes enfants, car il se déroule logiquement et conduit à d'excellentes expériences utilisateur.
Par exemple, dans Kubectl, vous verrez ce paradigme partout : « kubectl get pods », « kubectl apply… » ou « kubectl label pods… » Cela garantit un flux sensé dans la façon dont les utilisateurs interagiront avec votre ligne de commande. application et aide beaucoup lorsque l'on parle de commandes avec d'autres personnes.
En fin de compte, cette structure et cette suggestion peuvent vous éclairer sur la façon dont vous organisez vos fichiers et répertoires, mais encore une fois, c'est à vous de déterminer comment vous structurez votre CLI et présentez le flux aux utilisateurs finaux.
Dans la CLI Pizza, nous avons une structure bien définie dans laquelle vivent les commandes enfants (et les petits-enfants ultérieurs de ces commandes enfants). Sous le répertoire cmd de leurs propres packages, chaque commande obtient sa propre implémentation. L'échafaudage de commande racine existe dans un répertoire pkg/utils car il est utile de considérer la commande racine comme un utilitaire de niveau supérieur utilisé par main.go, plutôt que comme une commande qui pourrait nécessiter beaucoup de maintenance. En règle générale, dans l'implémentation de votre commande racine Go, vous aurez beaucoup de paramètres de configuration standard auxquels vous ne toucherez pas beaucoup, donc c'est bien de supprimer ces éléments.
Voici une vue simplifiée de notre structure de répertoires :
├── main.go ├── pkg/ │ ├── utils/ │ │ └── root.go ├── cmd/ │ ├── Child command dir │ ├── Child command dir │ │ └── Grandchild command dir
Cette structure permet une séparation claire des préoccupations et facilite la maintenance et l'extension de la CLI à mesure qu'elle se développe et que nous ajoutons plus de commandes.
L'une des principales bibliothèques que nous utilisons dans Pizza-CLI est la bibliothèque go-git, une pure implémentation de git dans Go qui est hautement extensible. Lors de la génération CODEOWNERS, cette bibliothèque nous permet de parcourir le journal des références git, d'examiner les différences de code et de déterminer quels auteurs git sont associés aux attributions configurées définies par un utilisateur.
Itérer le journal git ref d'un dépôt git local est en fait assez simple :
// 1. Open the local git repository repo, err := git.PlainOpen("/path/to/your/repo") if err != nil { panic("could not open git repository") } // 2. Get the HEAD reference for the local git repo head, err := repo.Head() if err != nil { panic("could not get repo head") } // 3. Create a git ref log iterator based on some options commitIter, err := repo.Log(&git.LogOptions{ From: head.Hash(), }) if err != nil { panic("could not get repo log iterator") } defer commitIter.Close() // 4. Iterate through the commit history err = commitIter.ForEach(func(commit *object.Commit) error { // process each commit as the iterator iterates them return nil }) if err != nil { panic("could not process commit iterator") }
Si vous créez une application basée sur Git, je vous recommande vivement d'utiliser go-git : il est rapide, s'intègre bien dans l'écosystème Go et peut être utilisé pour faire toutes sortes de choses !
Notre équipe d'ingénierie et de produits est profondément investie pour offrir la meilleure expérience de ligne de commande possible à nos utilisateurs finaux : cela signifie que nous avons pris des mesures pour intégrer une télémétrie anonymisée qui peut signaler à Posthog l'utilisation et les erreurs dans la nature. Cela nous a permis de corriger d'abord les bogues les plus importants, de parcourir rapidement les demandes de fonctionnalités les plus courantes et de comprendre comment nos utilisateurs utilisent la CLI.
Posthog dispose d'une bibliothèque propriétaire dans Go qui prend en charge cette fonctionnalité exacte. Tout d'abord, nous définissons un client Posthog :
import "github.com/posthog/posthog-go" // PosthogCliClient is a wrapper around the posthog-go client and is used as a // API entrypoint for sending OpenSauced telemetry data for CLI commands type PosthogCliClient struct { // client is the Posthog Go client client posthog.Client // activated denotes if the user has enabled or disabled telemetry activated bool // uniqueID is the user's unique, anonymous identifier uniqueID string }
Ensuite, après avoir initialisé un nouveau client, nous pouvons l'utiliser via les différentes méthodes struct que nous avons définies. Par exemple, lors de la connexion à la plateforme OpenSauced, nous capturons des informations spécifiques sur une connexion réussie :
// CaptureLogin gathers telemetry on users who log into OpenSauced via the CLI func (p *PosthogCliClient) CaptureLogin(username string) error { if p.activated { return p.client.Enqueue(posthog.Capture{ DistinctId: username, Event: "pizza_cli_user_logged_in", }) } return nil }
Lors de l'exécution de la commande, les différentes fonctions de « capture » sont appelées pour capturer les chemins d'erreur, les chemins heureux, etc.
Pour les identifiants anonymisés, nous utilisons l'excellente bibliothèque UUID Go de Google :
newUUID := uuid.New().String()
Ces UUID sont stockés localement sur les machines des utilisateurs finaux au format JSON dans leur répertoire personnel : ~/.pizza-cli/telemtry.json. Cela donne à l'utilisateur final une autorité et une autonomie complètes pour supprimer ces données de télémétrie s'il le souhaite (ou désactiver complètement la télémétrie via les options de configuration !) afin de garantir qu'il reste anonyme lorsqu'il utilise la CLI.
Notre équipe d'ingénierie simplifiée suit un processus de développement itératif, en se concentrant sur la fourniture rapide de petites fonctionnalités testables. Généralement, nous le faisons via des problèmes GitHub, des demandes d'extraction, des jalons et des projets. Nous utilisons largement le cadre de test intégré de Go, écrivant des tests unitaires pour des fonctions individuelles et des tests d'intégration pour des commandes entières.
Malheureusement, la bibliothèque de tests standard de Go ne dispose pas d'une excellente fonctionnalité d'assertion prête à l'emploi. Il est assez facile d'utiliser « == » ou d'autres opérandes, mais la plupart du temps, lorsque l'on revient en arrière et que l'on lit les tests, il est agréable de pouvoir observer ce qui se passe avec des assertions comme « assert.Equal » ou « assert.Nil ». ".
Nous avons intégré l'excellente bibliothèque testify avec sa fonctionnalité « assert » pour permettre une mise en œuvre plus fluide des tests :
config, _, err := LoadConfig(nonExistentPath) require.Error(t, err) assert.Nil(t, config)
Nous utilisons beaucoup Just at OpenSauced, un utilitaire d'exécution de commandes, un peu comme le « make » de GNU, pour exécuter facilement de petits scripts. Cela nous a permis d'intégrer rapidement de nouveaux membres de l'équipe ou de la communauté vers notre écosystème Go, car la construction et les tests sont aussi simples que « simplement construire » ou « simplement tester » !
Par exemple, pour créer un utilitaire de build simple dans Just, au sein d'un fichier just, nous pouvons avoir :
build: go build main.go -o build/pizza
Ce qui construira un binaire Go dans le répertoire build/. Désormais, construire localement est aussi simple que d'exécuter une commande « juste ».
Mais nous avons pu intégrer davantage de fonctionnalités dans l'utilisation de Just et en avons fait la pierre angulaire de la façon dont l'ensemble de notre cadre de construction, de test et de développement est exécuté. Par exemple, pour créer un binaire pour l'architecture locale avec des variables de temps de construction injectées (comme le sha avec lequel le binaire a été construit, la version, la date et l'heure, etc.), nous pouvons utiliser l'environnement local et exécuter des étapes supplémentaires dans le script. avant d'exécuter le « go build » :
build: #!/usr/bin/env sh echo "Building for local arch" export VERSION="${RELEASE_TAG_VERSION:-dev}" export DATETIME=$(date -u "%Y-%m-%d-%H:%M:%S") export SHA=$(git rev-parse HEAD) go build \ -ldflags="-s -w \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza
Nous avons même étendu cela pour permettre la construction de plusieurs architectures et systèmes d'exploitation : Go utilise les variables d'environnement GOARCH et GOOS pour savoir sur quelle architecture de processeur et sur quel système d'exploitation s'appuyer. Pour créer d'autres variantes, nous pouvons créer des commandes Just spécifiques à cet effet :
# Builds for Darwin linux (i.e., MacOS) on arm64 architecture (i.e. Apple silicon) build-darwin-arm64: #!/usr/bin/env sh echo "Building darwin arm64" export VERSION="${RELEASE_TAG_VERSION:-dev}" export DATETIME=$(date -u "%Y-%m-%d-%H:%M:%S") export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="darwin" export GOARCH="arm64" go build \ -ldflags="-s -w \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH}
Construire la CLI Pizza à l'aide de Go et Cobra a été une aventure passionnante et nous sommes ravis de la partager avec vous. La combinaison des performances et de la simplicité de Go avec la puissante structuration des commandes de Cobra nous a permis de créer un outil non seulement robuste et puissant, mais également convivial et maintenable.
Nous vous invitons à explorer le référentiel GitHub de Pizza CLI, à essayer l'outil et à nous faire part de votre avis. Vos commentaires et contributions sont inestimables alors que nous travaillons à faciliter la gestion de la propriété du code pour les équipes de développement du monde entier !
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3