Introdução a Cuelang

Elton Minetto - Nov 8 '22 - - Dev Community

Aposto que nesse momento uma frase paira na sua cabeça:

"Mais uma linguagem de programação"?

Calma, calma, vem comigo que vai fazer sentido :)

Diferente de outras linguagens como Go ou Rust, que são de "propósito geral", a CUE pussui alguns propósitos bem específicos. O seu nome na verdade é uma sigla que significa "Configure Unify Execute" e segundo a documentação oficial:

Embora a linguagem não seja uma linguagem de programação de uso geral, ela possui muitas aplicações, como validação e modelagem de dados, configuração, consulta, geração de código e até script.

Ela é descrita como um "superset de JSON" e fortemente inspirada em Go. Ou como eu gosto de pensar:

"imagine que Go e JSON tiveram um tórrido romançe e o fruto dessa união foi CUE" :D

Neste post eu vou apresentar dois cenários onde a linguagem pode ser usada, mas a documentação oficial tem mais exemplos e uma boa quantia de informação importante a ser consultada.

Validando dados

O primeiro cenário onde CUE se destaca é na validação de dados. Ela possui suporte nativo para validar YAML, JSON, Protobuf, entre outros.

Vou usar como case alguns exemplos de arquivos de configuração do projeto Traefik, um API Gateway.

O YAML a seguir define uma rota válida para o Traefik:

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: simpleingressroute
  namespace: default
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`your.example.com`) && PathPrefix(`/notls`)
      kind: Rule
      services:
        - name: whoami
          port: 80
Enter fullscreen mode Exit fullscreen mode

Com essa informação é possível definirmos uma nova rota no API Gateway, mas se algo estiver errado podemos causar alguns problemas. Por isso é importante termos uma forma fácil de detectarmos problemas em arquivos de configuração como esse. E é aí que a CUE mostra sua força.

O primeiro passo é termos a linguagem instalada na máquina. Como estou usando macOS bastou executar o comando:

brew install cue-lang/tap/cue
Enter fullscreen mode Exit fullscreen mode

Na documentação oficial é possível ver como fazer a instalação em outros sistemas operacionais.

Agora podemos usar o comando cue para transformar esse YAML em um schema da linguagem CUE:

cue import traefik-simple.yaml
Enter fullscreen mode Exit fullscreen mode

É criado um arquivo chamado traefik-simple.cue com o conteúdo:

apiVersion: "traefik.containo.us/v1alpha1"
kind:       "IngressRoute"
metadata: {
    name:      "simpleingressroute"
    namespace: "default"
}
spec: {
    entryPoints: [
        "web",
    ]
    routes: [{
        match: "Host(`your.example.com`) && PathPrefix(`/notls`)"
        kind:  "Rule"
        services: [{
            name: "whoami"
            port: 80
        }]
    }]
}
Enter fullscreen mode Exit fullscreen mode

Ele é uma tradução literal do YAML para CUE, mas vamos editá-lo para criarmos algumas regras de validação. O conteúdo final do traefik-simple.cue ficou desta forma:

apiVersion: "traefik.containo.us/v1alpha1"
kind:       "IngressRoute"
metadata: {
    name:      string
    namespace: string
}
spec: {
    entryPoints: [
        "web",
    ]
    routes: [{
        match: string
        kind:  "Rule"
        services: [{
            name: string
            port: >0 & <= 65535
        }]
    }]
}
Enter fullscreen mode Exit fullscreen mode

Alguns dos itens ficaram exatamente iguais, como apiVersion: "traefik.containo.us/v1alpha1" e kind: "IngressRoute". Isso significa que esses são os valores exatos que estão esperados em todos os arquivos que serão validados por esse schema. Qualquer valor diferente destes vai ser considerado um erro. Outras informações foram alteradas, como:

metadata: {
    name:      string
    namespace: string
}
Enter fullscreen mode Exit fullscreen mode

Neste trecho definimos que o conteúdo do name, por exemplo, pode ser qualquer string válida. No trecho port: >0 & <= 65535 estamos fazendo uma validação importante ao definir que este campo só pode aceitar um número que seja maior do que 0 e menor ou igual a 65535.

Agora é possível validar se o conteúdo do YAML está de acordo com o schema usando o comando:

cue vet traefik-simple.cue traefik-simple.yaml
Enter fullscreen mode Exit fullscreen mode

Se tudo estiver correto nada é apresentado na linha de comando. Para demonstrar o funcionamento eu fiz uma alteração no traefik-simple.yaml mudando o valor do port para 0. Ao executar o comando novamente é possível ver o erro:

cue vet traefik-simple.cue traefik-simple.yaml
spec.routes.0.services.0.port: invalid value 0 (out of bound >0):
    ./traefik-simple.cue:16:10
    ./traefik-simple.yaml:14:18
Enter fullscreen mode Exit fullscreen mode

Se alterarmos algum dos valores esperados, como por exemplo kind: IngressRoute para algo diferente, como kind: Ingressroute o resultado é um erro de validação:

cue vet traefik-simple.cue traefik-simple.yaml
kind: conflicting values "IngressRoute" and "Ingressroute":
    ./traefik-simple.cue:2:13
    ./traefik-simple.yaml:2:8
Enter fullscreen mode Exit fullscreen mode

Desta forma é muito fácil encontrar algum erro em uma configuração de rotas do Traefik. O mesmo pode ser aplicado para outros formatos como JSON, Protobuf, arquivos do Kubernetes, etc.

Vejo um cenário muito claro de uso desse poder de validação de dados: adicionar um passo em CI/CDs para usar CUE e validar configurações em tempo de build, evitando problemas em deploy e execução de aplicações. Outro cenário é adicionar os comandos em um hook de Git, para validar as configurações ainda em ambiente de desenvolvimento.

Outra característica interessante da CUE é a possibilidade de criarmos packages, que contém uma série de schemas e que podem ser compartilhados entre projetos, da mesma forma que um package de Go. Na documentação oficial é possível ver como user esse recurso, bem como usar alguns packages nativos da linguagem, como strigs, lists, regex etc. Vamos usar um package no próximo exemplo.

Configurando aplicações

Outro cenário de uso da CUE é como linguagem de configuração de aplicações. Quem me conhece sabe que eu não tenho nenhum apreço por YAML (para dizer o mínimo) então qualquer outra opção chama minha atenção. Mas CUE tem algumas vantagens interessantes como:

  • por ser baseado em JSON torna a leitura e escrita muito mais simples (opinião minha)
  • resolve alguns problemas de JSON como a falta de comentários, o que é uma vantagem para YAML
  • por ser uma linguagem completa, é possível usar if, loop, pacotes embutidos na linguagem, herança de tipos, etc.

Para este exemplo o primeiro passo foi criar um pacote para armazenar nossa configuração. Para isso criei um diretório chamado config e dentro dele um arquivo chamado config.cue com o conteúdo:

package config 

db: {
    user:     "db_user"
    password: "password"
    host:     "127.0.0.1"
    port:     3306
}

metric: {
    host: "http://localhost"
    port: 9091
}

langs: [
    "pt_br",
    "en",
    "es",
]

Enter fullscreen mode Exit fullscreen mode

O próximo passo foi criar a aplicação que faz a leitura da configuração:

package main

import (
    "fmt"

    "cuelang.org/go/cue"
    "cuelang.org/go/cue/load"
)

type Config struct {
    DB struct {
        User string
        Password string
        Host string
        Port int
    }
    Metric struct {
        Host string
        Port int
    }
    Langs []string
}

// LoadConfig loads the Cue config files, starting in the dirname directory.
func LoadConfig(dirname string) (*Config, error) {
    cueConfig := &load.Config{
        Dir:        dirname,
    }

    buildInstances := load.Instances([]string{}, cueConfig)
    runtimeInstances := cue.Build(buildInstances)
    instance := runtimeInstances[0]

    var config Config
    err := instance.Value().Decode(&config)
    if err != nil {
        return nil, err
    }
    return &config, nil
}

func main() {
    c, err := LoadConfig("config/")
    if err != nil {
        panic("error reading config")
    }
    //a struct foi preenchida com os valores
    fmt.Println(c.DB.Host)
}

Enter fullscreen mode Exit fullscreen mode

Uma vantagem do conceito de package da CUE é que podemos quebrar a nossa configuração em arquivos menores, cada um com sua funcionalidade. Por exemplo, dentro do diretório config eu dividi o config.cue em arquivos distintos:

config/db.cue

package config 

db: {
    user:     "db_user"
    password: "password"
    host:     "127.0.0.1"
    port:     3306
}
Enter fullscreen mode Exit fullscreen mode

config/metric.cue

package config 

metric: {
    host: "http://localhost"
    port: 9091
}
Enter fullscreen mode Exit fullscreen mode

config/lang.cue

package config 

langs: [
    "pt_br",
    "en",
    "es",
]
Enter fullscreen mode Exit fullscreen mode

E não foi necessário alterar nada no arquivo main.go para que as configurações sejam carregadas. Com isso podemos ter uma separação melhor dos conteúdos das configurações, sem impacto no código da aplicação.

Conclusão

Neste post eu apenas "arranhei a superfície" do que é possível fazer com a CUE. Ela vem chamando atenção e sendo adotada em projetos importantes como o Istio, que usa para gerar schemes OpenAPI e CRDs para Kubernetes, e o Dagger. Me parece uma ferramenta que pode ser muito útil para uma série de projetos, em especial devido ao seu poder de validação de dados. E como um substituto para YAML, para minha alegria pessoal :D

Publicado originalmente em https://eltonminetto.dev no dia 08/11/2022.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .