ops/monitoring: deploy grafana

This is a basic grafana running on:

    https://monitoring-global-dashboard.k0.hswaw.net/

It contains a data source pointing at the corresponding global victoria
metrics. There's no dashboards, these will be provisioned soon via
jsonnet/grafonnet.

Change-Id: I84873bc323d1727096e3ce818fae122a9af3e191
diff --git a/ops/monitoring/k0.jsonnet b/ops/monitoring/k0.jsonnet
index 62810c5..30dd687 100644
--- a/ops/monitoring/k0.jsonnet
+++ b/ops/monitoring/k0.jsonnet
@@ -9,6 +9,7 @@
         storageClasses+: {
             prometheus: "waw-hdd-redundant-3",
             victoria: "waw-hdd-redundant-3",
+            grafana: "waw-hdd-redundant-3",
         },
     },
 
@@ -25,15 +26,22 @@
     // Global tier - victoria metrics.
     global: global.Global("k0") {
         cfg+: cfg {
+            oauth: {
+                clientId: "22659ba3-c8b2-4855-9553-f78884e0d743",
+                clientSecret: std.split(importstr "secrets/plain/global-oauth-client-secret", "\n")[0],
+            },
             hosts: {
                 globalAPI: "monitoring-global-api.k0.hswaw.net",
+                globalDashboard: "monitoring-global-dashboard.k0.hswaw.net",
             },
             agents: [
                 // Ingestion from k0 cluster tier.
                 { username: k0.cluster.cfg.username, password: std.split(importstr "secrets/plain/global-agent-cluster-k0", "\n")[0], },
-                // Access from q3k's test Grafana.
-                { username: "grafana", password: std.split(importstr "secrets/plain/global-agent-grafana", "\n")[0], },
             ],
+            loopbackGrafanaUser: {
+                username: "grafana",
+                password: std.split(importstr "secrets/plain/global-agent-grafana", "\n")[0],
+            },
         }, 
     },
 }
diff --git a/ops/monitoring/lib/global.libsonnet b/ops/monitoring/lib/global.libsonnet
index dbdbebb..f001c99 100644
--- a/ops/monitoring/lib/global.libsonnet
+++ b/ops/monitoring/lib/global.libsonnet
@@ -18,11 +18,13 @@
             images: {
                 victoria: "victoriametrics/victoria-metrics:v1.40.0",
                 vmauth: "victoriametrics/vmauth:v1.40.0",
+                grafana: "grafana/grafana:7.2.1",
             },
 
             hosts: {
                 // DNS hostname that this global tier will use. Ingress will run under it.
                 globalAPI: error "hosts.globalAPI must be set",
+                globalDashboard: error "hosts.globalDashboard must be set",
             },
 
             storageClasses: {
@@ -30,6 +32,11 @@
                 victoria: error "storageClasses.victoria must be set",
             },
 
+            oauth: {
+                clientId: error "oauth.clientId must be set",
+                clientSecret: error "oauth.clientSecret must be set",
+            },
+
             // A list of agents that will push metrics to this instance.
             // List of:
             // {
@@ -41,10 +48,14 @@
 
         // Generated URLs that agents should use to ship metrics over. Both require HTTP basic
         // auth, configured via cfg.agents.
-        // The internal URL should be used for agents colocated in the same Kubernetes cluster.
+        // The internal URL that should be used for agents colocated in the same Kubernetes cluster.
         internalIngestURL:: "http://%s/api/v1/write" % [global.victoria.serviceAPI.host_colon_port],
-        // The glboal URL should be used for agents sending data over the internet.
+        // The internal URL that should be used for readers colocated in the same Kubernetes cluster.
+        internalReadURL:: "http://%s/" % [global.victoria.serviceAPI.host_colon_port],
+        // The global URL that should be used for agents sending data over the internet.
         globalIngestURL:: "https://%s/api/v1/write" % [cfg.hosts.globalAPI],
+        // The global URL that should be used for readers over the internet.
+        globalReadURL:: "https://%s" % [cfg.hosts.globalAPI],
 
         namespace: kube.Namespace(cfg.namespace),
         local ns = global.namespace,
@@ -73,7 +84,7 @@
                                 password: a.password,
                                 url_prefix: "http://localhost:8428",
                             }
-                            for a in cfg.agents
+                            for a in (cfg.agents + [cfg.loopbackGrafanaUser])
                         ],
                     }) + "\n")
                 },
@@ -145,5 +156,150 @@
                 },
             },
         },
+
+        grafana: {
+            local grafana = self,
+
+            // grafana.ini, serialized to secret.
+            ini:: {
+                sections: {
+                    "auth": {
+                        "disable_login_form": true,
+                        "oauth_auto_login": true,
+                    },
+                    "security": {
+                        # We do not disable basic auth, as we want to use builtin
+                        # users as API users (eg for config reload), but we want
+                        # to disable the default admin:admin user.
+                        "disable_initial_admin_creation": true,
+                    },
+                    "auth.generic_oauth": {
+                        enabled: true,
+                        client_id: cfg.oauth.clientId,
+                        client_secret: cfg.oauth.clientSecret,
+                        auth_url: "https://sso-v2.hackerspace.pl/oauth/authorize",
+                        token_url: "https://sso-v2.hackerspace.pl/oauth/token",
+                        api_url: "https://sso-v2.hackerspace.pl/api/1/userinfo",
+                        scopes: "openid",
+                        email_attribute_path: "email",
+                        allow_sign_up: true,
+                        role_attribute_path: "contains(groups, 'grafana-admin')",
+                    },
+                    "server": {
+                        domain: cfg.hosts.globalDashboard,
+                        root_url: "https://%s/" % [ cfg.hosts.globalDashboard ],
+                    },
+                },
+            },
+
+            datasources:: {
+                apiVersion: 1,
+                datasources: [
+                    {
+                        name: "victoria-global",
+                        type: "prometheus",
+                        uid: "victoria-global",
+                        isDefault: true,
+                        url: global.internalReadURL,
+                        basicAuth: true,
+                        basicAuthUser: cfg.loopbackGrafanaUser.username,
+                        secureJsonData: {
+                            basicAuthPassword: cfg.loopbackGrafanaUser.password,
+                        },
+                    },
+                ],
+            },
+
+            config: ns.Contain(kube.Secret("grafana-config")) {
+                data+: {
+                    "grafana.ini": std.base64(std.manifestIni(grafana.ini)),
+                    "datasources.yaml": std.base64(std.manifestYamlDoc(grafana.datasources)),
+                },
+            },
+
+            pvc: ns.Contain(kube.PersistentVolumeClaim("grafana-data")) {
+                spec+: {
+                    storageClassName: cfg.storageClasses.grafana,
+                    accessModes: ["ReadWriteOnce"],
+                    resources: {
+                        requests: {
+                            storage: "8Gi",
+                        },
+                    },
+                },
+            },
+
+            deploy: ns.Contain(kube.Deployment("grafana")) {
+                spec+: {
+                    template+: {
+                        spec+: {
+                            containers_: {
+                                default: kube.Container("default") {
+                                    image: cfg.images.grafana,
+                                    ports_: {
+                                        public: { containerPort: 3000 },
+                                    },
+                                    env_: {
+                                        GF_PATHS_CONFIG: "/etc/hscloud-config/grafana.ini",
+                                        GF_PATHS_PROVISIONING: "/etc/hscloud-config/provisioning",
+                                        GF_PATHS_DATA: "/var/lib/grafana",
+                                    },
+                                    volumeMounts_: {
+                                        config: { mountPath: "/etc/hscloud-config", },
+                                        data: { mountPath: "/var/lib/grafana", },
+                                    },
+                                    resources: {
+                                        requests: { cpu: "100m", memory: "256M", },
+                                        limits: { cpu: "200m", memory: "512M", },
+                                    },
+                                },
+                            },
+                            volumes_: {
+                                data: kube.PersistentVolumeClaimVolume(grafana.pvc),
+                                config: kube.SecretVolume(grafana.config) {
+                                    secret+: {
+                                        items: [
+                                            { key: "grafana.ini", path: "grafana.ini", },
+                                            { key: "datasources.yaml", path: "provisioning/datasources/datasources.yaml", },
+                                        ],
+                                    },
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+
+            service: ns.Contain(kube.Service("grafana-public")) {
+                target_pod: grafana.deploy.spec.template,
+                spec+: {
+                    ports: [
+                        { name: "public", port: 3000, targetPort: 3000, protocol: "TCP" },
+                    ],
+                },
+            },
+
+            ingress: ns.Contain(kube.Ingress("grafana-public")) {
+                metadata+: {
+                    annotations+: {
+                        "kubernetes.io/tls-acme": "true",
+                        "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+                    },
+                },
+                spec+: {
+                    tls: [
+                        { hosts: [cfg.hosts.globalDashboard], secretName: "ingress-grafana-tls" },
+                    ],
+                    rules: [
+                        {
+                            host: cfg.hosts.globalDashboard,
+                            http: {
+                                paths: [ { path: "/", backend: { serviceName: grafana.service.metadata.name, servicePort: 3000 } }, ],
+                            },
+                        }
+                    ],
+                },
+            },
+        },
     }
 }
diff --git a/ops/monitoring/secrets/cipher/global-oauth-client-secret b/ops/monitoring/secrets/cipher/global-oauth-client-secret
new file mode 100644
index 0000000..77b4e89
--- /dev/null
+++ b/ops/monitoring/secrets/cipher/global-oauth-client-secret
@@ -0,0 +1,40 @@
+-----BEGIN PGP MESSAGE-----
+
+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
+=8iTI
+-----END PGP MESSAGE-----