dynatrace-extensions / dynatrace-extensions-vscode

A VisualStudio Code extension to support all aspects of developing Dynatrace Extensions 2.0
Apache License 2.0
16 stars 7 forks source link

Platform native support 1 - SDKs and OAuth #163

Open radu-stefan-dt opened 8 months ago

radu-stefan-dt commented 8 months ago

Summary First part of achieving Dynatrace platform-native support. Platform access is controlled via SDKs or APIs using OAuth authentication scheme. To truly support platform natively we must also have a client that can interact this way with the tenant.

Details SDKs would be preferred here since they tend to offer an easier way to work with these operations but a while back it wasn't possible to use them outside of Apps. APIs would be the alternative.

Sub-tasks for this feature:

Use Cases

Challenges

Other notes SSO Login workflow (like in AppToolkit) might be a solid alternative to OAuth clients. Worth investigating.

radu-stefan-dt commented 8 months ago

Simple example on how one might use SDKs to create a platform client for this ticket.

import { QueryExecutionClient } from "@dynatrace-sdk/client-query";
import {
  PlatformHttpClient,
  RequestBodyTypes,
  HttpClientRequestOptions,
  HttpClientResponse,
  PlatformHttpClientResponse,
} from "@dynatrace-sdk/http-client";

const BASE_URL = "https://base.url.to.dynatrace";
const CLIENT_ID = "dt0s02.XXXXXXXX";
const CLIENT_SECRET = "dt0s02.XXXXXXXX.YYYYYYYYYYYYYYYYYYYYYYY";
const ACCOUNT_URN = "urn:dtaccount:aaaaaa-bbbb-cccc";
// Example for Sprint SSO. In reality, this will have to be dynamic.
const SSO_URL = "https://sso-sprint.dynatracelabs.com/sso/oauth2/token"

class SamplePlatformClient implements PlatformHttpClient {
  private readonly ssoUrl = SSO_URL;
  private readonly baseUrl: string;
  private readonly clientId: string;
  private readonly clientSecret: string;
  private readonly accountUrn: string;

  constructor(baseUrl: string, clientId: string, clientSecret: string, accountUrn: string) {
    this.baseUrl = baseUrl;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.accountUrn = accountUrn;
  }

  private async getBearer() {
    const tokenResponse = await fetch(this.ssoUrl, {
      method: "POST",
      headers: {
        "content-type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        grant_type: "client_credentials",
        client_id: this.clientId,
        client_secret: this.clientSecret,
        scope: "storage:logs:read storage:buckets:read",
        resource: this.accountUrn,
      }).toString(),
    });
    const tokenData = (await tokenResponse.json()) as { access_token: string };
    return tokenData.access_token;
  }

  async send<T extends keyof RequestBodyTypes = "json">({
    url,
    method,
    headers,
    body,
    abortSignal,
  }: HttpClientRequestOptions<T>): Promise<HttpClientResponse> {
    const token = await this.getBearer();
    const requestConfig = {
      method: method,
      signal: abortSignal,
      headers: {
        ...headers,
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(body),
    };
    const response = await fetch(`${this.baseUrl}${url}`, requestConfig);
    return new PlatformHttpClientResponse(response);
  }
}

const httpClient = new SamplePlatformClient(BASE_URL, CLIENT_ID, CLIENT_SECRET, ACCOUNT_URN);
const dqlClient = new QueryExecutionClient(httpClient);

dqlClient
  .queryExecute({
    body: {
      query:
        "fetch logs, from: now() - 2h | summarize count=count(), by:{bin(timestamp, 10m), loglevel}",
      requestTimeoutMilliseconds: 30000,
    },
  })
  .then(res => {
    console.info(res);
  })
  .catch(err => {
    console.error(err);
  });

Things to consider: