terraform-provider-github icon indicating copy to clipboard operation
terraform-provider-github copied to clipboard

[Feature Request] Manage GitHub Apps via Terraform

Open smauermann opened this issue 4 years ago • 22 comments

Hey Team,

I was looking for a way to manage GitHub apps [1] via Terraform but quickly realized there is none. It would be great to be able to setup a GitHub app, here are some exemplary settings one might want to manage:

  • app name
  • permissions
  • webhook URL and secret
  • event subscriptions

Cheers!

Terraform Version

Terraform v0.12.28

References

[1] https://developer.github.com/apps/about-apps/

smauermann avatar Jul 07 '20 14:07 smauermann

It would be great to be able to setup a GitHub app

I've been playing around with this a little bit, and the public API support for GitHub Apps isn't where it needs to be in order to support standard CRUD operations on a GitHub App. I've spoken with the team internally and they're aware of the issue but it isn't an immediate priority.

The GraphQL API doesn't support App operations at the moment.

The auth used for creating Apps on the go-github package uses screen-scraping, which requires a One Time Password and which might rule out using it as part of the Terraform provider; I have some reservations about the stability of such a package.

In any event, I wanted to create a proof of concept, so I created a sample app with hard-coded values but I haven't been able to successfully get an App created. When skipping the OTP, I get a 406 Not Acceptable each time, and when attempting to provide a OTP through my standard 2FA app, the client panics when authenticating.

There might be something I'm missing on how the screen-scraping package of the google/go-github module is supposed to work. If there is, I'd love to hear about it!

In the meantime, we'll look into what operations we can support given an existing App.

kfcampbell avatar Aug 19 '21 17:08 kfcampbell

I've investigated a little bit more on how to programmatically support the requested GitHub Apps operations.

  • app name

I can't find anything in the API documentation about changing the name. I don't think the API that works behind the UI is public.

  • permissions

Can be managed when creating an installation, but updating doesn't appear to be supported in the public API either.

  • webhook URL and secret

REST docs are here, but that functionality doesn't appear to be available in go-github yet.

  • event subscriptions

I believe these are managed through the same API as permissions above.

It definitely feels like we're working without tools on this feature a bit.

kfcampbell avatar Aug 19 '21 21:08 kfcampbell

Thanks for scoping this out and reporting back to the rest of us on this. Definitely feels like the wrong time to be implementing automation around GitHub App creation as support for this via api.github.com is limited with no enhancements planned in the near-term.

My suggestion is we park this for now given the complexity of the creation path. However, we could pave paths for import-and-manage style workflows if there is more feedback from the community that this would be used.

jcudit avatar Aug 31 '21 19:08 jcudit

This would definitely be of huge value. We are heading towards of managing dozens of GitHub Apps in our organization. For transparency reasons and documentation it would be great to have them as IaC.

Since github_app_installation_repository is already implemented, this would simply be awesome.

n3ph avatar Apr 05 '22 15:04 n3ph

I'm really craving for this feature...

calexandre avatar Aug 26 '22 18:08 calexandre

We manage tens of github apps across 3 orgs (github enterprise) in our company. Changing everything manually and keeping in sync is PITA. E.g. recently I have to change "subscribe to events" in all apps, so I planned to write a tf module to manage our github app and automate that, but I was shocked there is no terraform support for it apparently :(

mwos-sl avatar Sep 05 '22 09:09 mwos-sl

I've asked around internally and linked this issue along to the team. I know there's a lot of interest in this recorded. The team is looking at it but is not working on it presently and I can't offer a timeframe now, unfortunately.

kfcampbell avatar Sep 08 '22 16:09 kfcampbell

Thank you for that @kfcampbell , there's definitely keen interest!

ericsampson avatar Feb 01 '23 18:02 ericsampson

Also, and I'm not sure if this is the right issue to ask for it, it'd be nice to have a fine grained token that'd allow to install one or more specific apps, while not allowing to install other apps, in some or all org repos. Having a GitOps operation where there's an admin PAT running code is dangerous and right now we are forced to run the tf apply manually on our own terminal, that's no good.

suvl avatar Feb 02 '23 10:02 suvl

+1 this would be a lovely feature. Is there any update on if this might be explored?

ldevore-indigoag avatar May 10 '23 16:05 ldevore-indigoag

I agree that this would be an excellent feature. I've seen some more people asking for it internally, which is good, but no idea on a timeframe for implementation/release yet unfortunately.

kfcampbell avatar May 15 '23 20:05 kfcampbell

+1 here. We also recently started to use github_enterprise_organization but now we miss managing app for those organization

jonesbusy avatar May 16 '23 06:05 jonesbusy

+1 here. At my day job at $very_large_corp we could really use this. At the moment it introduces a manual step in an otherwise largely automated process.

riccardofreixo avatar Aug 18 '23 10:08 riccardofreixo

+1 here

My team has to be able to add/remove thousands, yes, thousands of repositories from a given GitHub Application "Repository Access", having to manually perform this task is madness.

Terraform Integrations/github provider does support github_app_installation_repositories but that is all about it.

Does anybody know any workaround for now?? API calls or anything that can be somewhat automated?? This is a 2020 request and so far it looks like nothing was really done :(

Thanks

HakunMatat4 avatar Sep 05 '23 01:09 HakunMatat4

+1 here

Spend a couple of hours today trying to achieve a behavior that depended on accesses managed by a Github app, if the feature was available the visibility would be in plain sight so the access could be managed easily in the IaC scenario I'm working on.

MarconiGRF avatar Sep 26 '23 18:09 MarconiGRF

it seems there is a rest api for it now https://github.com/google/go-github/blob/master/github/apps.go#L16

peter-romfeld-bcw avatar Oct 19 '23 14:10 peter-romfeld-bcw

This part of the API has existed for some time, it can be used to manage app installations, but not apps

morremeyer avatar Oct 19 '23 14:10 morremeyer

+1 Registering interest in this one also - same as above it would be nice to fully automate this. Does anybody know where we can register interest in the required API change with github?

phill-lewis avatar Nov 22 '23 17:11 phill-lewis

@phill-lewis it would be worth filing a request here.

kfcampbell avatar Nov 22 '23 20:11 kfcampbell

Come on GitHub, almost 4 years later and we still don't have access to this resource🥲

ShankyJS avatar Mar 28 '24 23:03 ShankyJS

For what it's worth, if you're using pulumi here is a simple dynamic provider to manage GitHub App info I've been using internally for the past 6 months or so:

import * as pulumi from '@pulumi/pulumi';
import { Octokit } from '@octokit/core';
import { AppAuthentication, createAppAuth } from '@octokit/auth-app';

export interface ConfigurationResourceInputs {
  appId: pulumi.Input<string>;
  privateKey: pulumi.Input<string>;
  clientId: pulumi.Input<string>;
  clientSecret: pulumi.Input<string>;
  url: pulumi.Input<string>;
  secret: pulumi.Input<string>;
  contentType?: pulumi.Input<string> | undefined;
}

export class Configuration extends pulumi.dynamic.Resource {
  public readonly appId!: pulumi.Output<pulumi.ID>;
  public readonly privateKey!: pulumi.Output<string>;
  public readonly clientId!: pulumi.Output<string>;
  public readonly clientSecret!: pulumi.Output<string>;
  public readonly url!: pulumi.Output<string>;
  public readonly secret!: pulumi.Output<string>;
  public readonly contentType!: pulumi.Output<string>;

  constructor(name: string, args: ConfigurationResourceInputs, opts?: pulumi.CustomResourceOptions) {
    super(new ConfigurationProvider(), name, args, opts, 'github-app', 'Configuration');
  }
}

interface ConfigurationInputs {
  appId: string;
  privateKey: string;
  clientId: string;
  clientSecret: string;
  url: string;
  secret: string;
  contentType?: string | undefined;
}

export interface ConfigurationOutputs extends Omit<ConfigurationInputs, 'url' | 'secret'> {
  url?: string | undefined;
  secret?: string | undefined;
}

export class ConfigurationProvider implements pulumi.dynamic.ResourceProvider {
  async auth({
    appId,
    privateKey,
    clientId,
    clientSecret,
  }: Pick<
    ConfigurationInputs,
    'appId' | 'clientId' | 'clientSecret' | 'privateKey'
  >): Promise<AppAuthentication> {
    const auth = createAppAuth({
      appId: appId,
      privateKey: privateKey,
      clientId: clientId,
      clientSecret: clientSecret,
    });
    return await auth({ type: 'app' });
  }

  async create(inputs: ConfigurationInputs): Promise<pulumi.dynamic.CreateResult> {
    const auth = await this.auth(inputs);
    const octokit = new Octokit({ auth: auth.token });
    try {
      const { data } = await octokit.request('PATCH /app/hook/config', {
        data: JSON.stringify({
          url: inputs.url,
          secret: inputs.secret,
          insecure_ssl: '0',
          content_type: inputs.contentType || 'json',
        }),
      });
      return {
        id: inputs.appId,
        outs: {
          appId: inputs.appId,
          privateKey: inputs.privateKey,
          clientId: inputs.clientId,
          clientSecret: inputs.clientSecret,
          url: data.url,
          secret: data.secret,
          contentType: data.content_type,
        },
      };
    } catch (err) {
      await pulumi.log.error(`${JSON.stringify(err, null, 2)}`);
      throw new Error('Failed to set github app webhook config');
    }
  }

  async diff(
    _id: pulumi.ID,
    olds: ConfigurationOutputs,
    news: ConfigurationInputs,
  ): Promise<pulumi.dynamic.DiffResult> {
    let changes: boolean = false;
    let replaces: string[] = [];
    let keys = Object.keys(news) as Array<Extract<keyof typeof news, string>>;

    for (const key of keys) {
      if (JSON.stringify(olds[key]) !== JSON.stringify(news[key])) {
        changes = true;
        if (key === 'appId' || key === 'privateKey' || key === 'clientId' || key === 'clientSecret') {
          replaces.push(key);
        }
      }
    }

    return { changes, replaces };
  }

  async update(
    id: pulumi.ID,
    _olds: ConfigurationOutputs,
    news: ConfigurationInputs,
  ): Promise<pulumi.dynamic.UpdateResult> {
    const auth = await this.auth(news);
    const octokit = new Octokit({ auth: auth.token });
    try {
      const { data } = await octokit.request('PATCH /app/hook/config', {
        data: JSON.stringify({
          url: news.url,
          secret: news.secret,
          insecure_ssl: '0',
          content_type: news.contentType || 'json',
        }),
      });
      return {
        outs: {
          appId: id,
          privateKey: news.privateKey,
          clientId: news.clientId,
          clientSecret: news.clientSecret,
          url: data.url,
          secret: data.secret,
          contentType: data.content_type,
        },
      };
    } catch (err) {
      await pulumi.log.error(`${JSON.stringify(err, null, 2)}`);
      throw new Error('Failed to set github app webhook config');
    }
  }

  async read(id: pulumi.ID, props?: ConfigurationOutputs): Promise<pulumi.dynamic.ReadResult> {
    if (!props) {
      throw new Error('Props undefined');
    }
    const auth = await this.auth({ ...props, appId: id });
    const octokit = new Octokit({ auth: auth.token });
    try {
      const { data } = await octokit.request('GET /app/hook/config');
      return {
        id,
        props: {
          appId: id,
          privateKey: props.privateKey,
          clientId: props.clientId,
          clientSecret: props.clientSecret,
          url: data.url,
          secret: data.secret,
          contentType: data.content_type,
        },
      };
    } catch (err) {
      await pulumi.log.error(`${JSON.stringify(err, null, 2)}`);
      throw new Error('Failed to set github app webhook config');
    }
  }
}

Just in case that would help others who were trying to figure out how to deal with that situation. Pulumi's GitHub provider is generated based on the terraform provider sources, so faces the same problem documented in this thread.

dgellow avatar Mar 29 '24 11:03 dgellow

+1. I would love to contribute as well.

IreshMM avatar Jun 28 '24 09:06 IreshMM