En esta segunda entrada, el foco va a ser usar el paquete flag para que el usuario pueda proporcionar, por ejemplo, el projectId donde listar las API keys.

Paquete flag: sencillo y funcional

En el apartado anterior el parámetro projectId, necesario para indicar en qué proyecto queremos listar las API keys, se obtenía desde una variable de entorno. Sin embargo, queremos que el usuario puede especificar el valor del proyecto desde la línea de comando, por ejemplo, mediante --project <projec-id>.

Empezamos definiendo un nuevo flagSet, al que llamamos apikeycheck.

// ... before
func main() {
    ctx := context.Background()
    c, err := apikeys.NewClient(ctx)
    if err != nil {
    // ...
// ... after
func main() {
    fs := flag.NewFlagSet("apikeycheck", flag.ExitOnError)
    projectId := fs.String("project", "", "Check API keys in the project identified by ProjectId")
    if err := fs.Parse(os.Args[1:]); err != nil {
        fmt.Printf("error parsing flags: %s\n", err.Error())
        os.Exit(1)
    }

    ctx := context.Background()

De esta forma, el usuario puede proporcionar el projectId a través de la línea de comando mediante --project <projectid>. Lo único importante a tener en cuenta es que projectId es *string, no string. Por tanto, en la ListKeysRequest:

// ... before
    req := &apikeyspb.ListKeysRequest{
        // See https://pkg.go.dev/cloud.google.com/go/apikeys/apiv2/apikeyspb#ListKeysRequest.
        Parent: fmt.Sprintf("projects/%s/locations/global", os.Getenv("PROJECTID"),
    }
// ... after
    req := &apikeyspb.ListKeysRequest{
        // See https://pkg.go.dev/cloud.google.com/go/apikeys/apiv2/apikeyspb#ListKeysRequest.
        Parent: fmt.Sprintf("projects/%s/locations/global", *projectId),
    }

Validar que el projectId no está vacío

El paquete flag genera un error si se especifica un flag y no se le asigna un valor, como --project <nothing>, pero el valor "" es perfectamente válido: --project "". Pero si no especificamos el valor del projectId, la llamada a la API de las API keys fallará.

Además, como no hay ninguna forma de indicar que un flag es requerido, tenemos que encargarnos nosotros de realizar esas validaciones en nuestra aplicación por si el usuario no incluye --project.

La solución es validar que el valor de projectId no está vacío.

//  ... before
        fmt.Printf("error parsing flags: %s\n", err.Error())
        os.Exit(1)
    }

    ctx := context.Background()
//  ... after
        fmt.Printf("error parsing flags: %s\n", err.Error())
        os.Exit(1)
    }

    if *projectId == "" {
        fmt.Println("projectId cannot be empty")
        os.Exit(1)
    }

    ctx := context.Background()

De esta forma, si el usuario no proporciona un valor para --project:

$ ./apikeycheck
projectId cannot be empty

Ayuda incorporada

Una de las ventajas de usar el paquete flag es que la información que proporcionamos al definir los parámetros se muestra al usuario en forma de ayuda:

$ ./apikeycheck --help
Usage of apikeycheck:
  -project string
      Check API keys in the project identified by ProjectId

Periodo de rotación

El periodo recomendado de rotación de las API keys es de 90 días. Pero puede que el criterio para tu organización sea diferente; usando el paquete flag añadimos una nueva flag para que el usuario especifique su propio valor, en días, si así lo desea. Si no, la aplicación usará el valor de 90 por defecto.

Añadimos una nueva línea tras la declaración de projectId:

//  ... before
    fs := flag.NewFlagSet("apikeycheck", flag.ExitOnError)
    projectId := fs.String("project", "", "Check API keys in the project identified by ProjectId")
    if err := fs.Parse(os.Args[1:]); err != nil {
        // ...
//  ... after
    fs := flag.NewFlagSet("apikeycheck", flag.ExitOnError)
    projectId := fs.String("project", "", "Check API keys in the project identified by ProjectId")
    maxDays := fs.Int("max-days", 90, "Max. number of days before API keys should be rotated")
    if err := fs.Parse(os.Args[1:]); err != nil {
        // ...

Como hemos definido maxDays pero no lo usamos, el linter se queja; para evitarlo, asignamos temporalmente maxDays al blank identifier:

// ... temporary fix
    if err := fs.Parse(os.Args[1:]); err != nil {
        fmt.Printf("error parsing flags: %s\n", err.Error())
        os.Exit(1)
    }
    _ = maxDays

En la siguiente entrada, pondremos el foco en explorar las propiedades de las API keys para identificar cuáles nos pueden interesar para identificar las que deben ser rotadas.

Serie completa