Ejemplo n.º 1
0
def plan_repo_add(config, plan_id, repo):
    api_client = ApiClient(config)

    # Look up the plan
    plan = get_plan(api_client, plan_id)

    # Filter by repository
    repo_data = lookup_repo(api_client, config, repo, required=True)

    # Check that we don't already have a matching PlanRepository
    params = {
        'plan': plan_id,
        'repo': repo_data['id'],
    }
    res = api_client('plan_repos', 'list', params=params)
    if res['count']:
        raise click.ClickException('Repo already added to plan')

    # Create the PlanRepository
    params = {
        'plan_id': plan_id,
        'repo_id': repo_data['id'],
    }
    res = api_client('plan_repos', 'create', params=params)
    click.echo()
    click.echo(
        'Added repo {repo[owner]}/{repo[name]} to plan {plan[name]}'.format(
            **res))
    render_recursive(res)
Ejemplo n.º 2
0
def build_info(config, build_id, log, flow_log, flow):
    api_client = ApiClient(config)

    params = {
        'id': build_id,
    }

    # Look up the build
    try:
        build_res = api_client('builds', 'read', params=params)
    except coreapi.exceptions.ErrorMessage as e:
        raise click.ClickException(
            'Build with id {} not found.  Use metaci build list to see a list of latest builds and their ids'
            .format(build_id))

    if log:
        click.echo(build_res['log'])
    elif flow_log:
        params = {'build': build_id}
        if flow:
            params['flow'] = flow
        build_flow_res = api_client('build_flows', 'list', params=params)
        for build_flow in build_flow_res['results']:
            click.echo()
            click.echo(
                click.style('{}:'.format(build_flow['flow']),
                            bold=True,
                            fg='blue'))
            click.echo(build_flow['log'])

    else:
        click.echo(render_recursive(build_res))
Ejemplo n.º 3
0
def service_info(config, name):
    api_client = ApiClient(config)

    params = {'name': name}

    # Look up the service
    res = api_client('services', 'list', params=params)
    if res['count'] == 0:
        raise click.ClickError('Service named {} not found'.format(name))

    click.echo(render_recursive(res['results'][0]))
Ejemplo n.º 4
0
def org_info(config, name, repo):
    api_client = ApiClient(config)

    params = {'name': name}

    # Filter by repository
    repo_data = lookup_repo(api_client, config, repo, required=True)
    params['repo'] = repo_data['id']

    # Look up the org
    res = api_client('orgs', 'list', params=params)
    if res['count'] == 0:
        raise click.ClickError('Org named {} not found'.format(name))

    click.echo(render_recursive(res['results'][0]))
Ejemplo n.º 5
0
def plan_add(config):
    if not config.project_config:
        raise click.UsageError(
            'You must be inside a local git repository configured for CumulusCI.  No CumulusCI configuration was detected in the local directory'
        )

    api_client = ApiClient(config)

    params = {}

    # Make sure the local repo exists
    repo_data = lookup_repo(api_client, config, None, required=True)

    click.echo('# Name and Description')
    click.echo(
        'Provide a name and description of the build plan you are creating')
    name = click.prompt('Name')
    description = click.prompt('Description')

    click.echo()
    click.echo('# Org')
    click.echo(
        'Select the MetaCI org this plan should run its builds against.  If the org does not yet exist, use metaci org create to create the org first.'
    )
    org = prompt_org(api_client, config, repo_data['id'])

    flows_list = config.project_config.flows.keys()
    flows_list.sort()
    click.echo()
    click.echo('# Flows')
    click.echo(
        'What CumulusCI flows should this plan run?  You can enter multiple flows using a comma as a separator'
    )
    click.echo('Available Flows: {}'.format(', '.join(flows_list)))
    flows = click.prompt('Flow(s)')

    click.echo()
    click.echo('# Trigger Type')
    click.echo('How should this plan be triggered?')
    click.echo(
        '  - commit: Trigger on branch commits matching a regex pattern')
    click.echo('  - tag: Trigger on new tags matching a regex pattern')
    click.echo(
        '  - manual: Do not auto-trigger any builds.  Can be manually triggered.'
    )
    trigger_type = click.prompt('Trigger Type (commit, tag, or manual)')
    if trigger_type == 'commit':
        regex = click.prompt('Branch Match RegEx (ex feature/.*)')
    elif trigger_type == 'tag':
        regex = click.prompt('Tag Match RegEx (ex beta/.*)')
    elif trigger_type != 'manual':
        raise click.UsageError(
            '{} is an invalid trigger type.  Valid choices are: commit, tag, manual'
        )
    else:
        regex = None

    click.echo()
    click.echo('# Github Commit Status')
    click.echo(
        'MetaCI can set the build status via the Github Commit Status API.  Commit statuses are grouped by a context field.  Setting a different context on different plans will cause multiple commit statuses to be created for each unique context in Github.'
    )
    context = None
    if click.confirm('Set commit status in Github for this plan?',
                     default=True):
        context = click.prompt('Github Commit Status Context', default=name)

    if trigger_type == 'manual':
        active = True
    else:
        click.echo()
        click.echo('# Activate Plan')
        match_str = 'matching regex {}'.format(regex) if regex else ''
        click.echo(
            'If active, this plan will automatically build on new {}s{}'.
            format(trigger_type, match_str))
        active = click.confirm('Active?', default=True)

    click.echo()
    click.echo('# Public or Private')
    click.echo(
        'Public plans and their builds are visible to anonymous users.  This is recommended for open source projects, instances on an internal network or VPN, or projects that want transparency of their build information.  Private plans are only visible to logged in users who have been granted the Is Staff role by an admin.'
    )
    public = click.confirm('Public?')

    params = {
        'name': name,
        'description': description,
        'org': org,
        'flows': flows,
        'type': trigger_type,
        'regex': regex,
        'context': context,
        'active': active,
        'public': public,
    }

    res = api_client('plans', 'create', params=params)

    click.echo()
    click.echo(
        click.style(
            'Created plan {} with the following configuration'.format(name),
            fg='green',
        ))
    render_recursive(res)

    click.echo()
    click.echo('Adding repository {owner}/{name} to plan'.format(**repo_data))

    params = {
        'repo_id': repo_data['id'],
        'plan_id': res['id'],
    }
    res = api_client('plan_repos', 'create', params=params)
Ejemplo n.º 6
0
def plan_info(config, plan_id):
    api_client = ApiClient(config)
    plan = get_plan(api_client, plan_id)
    click.echo(render_recursive(plan))
Ejemplo n.º 7
0
def site_info(config):
    service = check_current_site(config)
    render_recursive(service.config)
Ejemplo n.º 8
0
def site_add(config, name, shape):
    if not config.project_config:
        raise click.ClickException('You must be in a CumulusCI configured local git repository.')
   
    verify_overwrite(config)

    # Initialize payload
    payload = {
        'app': {},
        'source_blob': {
            'url': 'https://github.com/SFDO-Tooling/MetaCI/tarball/master/',
       },
    }
    env = {} 

    # Heroku API Token
    try:
        token = subprocess.check_output(['heroku','auth:token']).strip()
    except:
        click.echo()
        click.echo(click.style('# Heroku API Token', bold=True, fg='blue'))
        click.echo('Enter your Heroku API Token.  If you do not have a token, go to the Account page in Heroku and use the API Token section: https://dashboard.heroku.com/account')
        click.echo(click.style(
            'NOTE: For security purposes, your input will be hidden.  Paste your API Token and hit Enter to continue.',
            fg='yellow',
        ))
        token = click.prompt('API Token', hide_input=True)

    heroku_api = prompt_heroku_token()

    # App Name
    if not name:
        click.echo()
        click.echo(click.style('# Heroku App Name', bold=True, fg='blue'))
        click.echo('Specify the name of the Heroku app you want to create.')
        payload['app']['name'] = click.prompt('App Name')
    else:
        payload['app']['name'] = name

    app_shape, num_workers = prompt_app_shape(shape)
    # Hirefire Token
    if app_shape == 'prod':
        click.echo()
        click.echo(click.style('# Hirefire Token', bold=True, fg='blue'))
        click.echo('The prod app shape requires the use of Hirefire.io to scale the build worker dynos.  You will need to have an account on Hirefire.io and get the API token from your account.')
        env['HIREFIRE_TOKEN'] = click.prompt('Hirefire API Token')

    # Salesforce DX
    click.echo()
    click.echo(click.style('# Salesforce DX Configuration', bold=True, fg='blue'))
    click.echo('The following prompts collect information from your local Salesforce DX configuration to use to configure MetaCI to use sfdx')

    # Salesforce DX Hub Key
    click.echo()
    click.echo('MetaCI uses JWT to connect to your Salesforce DX devhub.  Please enter the path to your local private key file.  If you have not set up JWT for your devhub, refer to the documentation: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_jwt_flow.htm')
    sfdx_private_key = click.prompt(
        'Path to private key',
        type=click.Path(readable=True, dir_okay=False),
        default=os.path.expanduser('~/.ssh/sfdx_server.key'),
    )
    with open(sfdx_private_key, 'r') as f:
        env['SFDX_HUB_KEY'] = f.read()

    # Salesforce DX Client ID
    click.echo()
    click.echo('Enter the Connected App Client ID you used for the JWT authentication flow to the Salesforce DX devhub.')
    click.echo(click.style(
        'NOTE: For security purposes, your input will be hidden.  Paste your Client ID and hit Enter to continue.',
        fg='yellow',
    ))
    env['SFDX_CLIENT_ID'] = click.prompt('Client ID', hide_input=True)

    # Salesforce DX Username
    try:
        devhub = subprocess.check_output(['sfdx','force:config:get', 'defaultdevhubusername', '--json']).strip()
        devhub = json.loads(devhub)
        devhub = devhub['result'][0]['value']
    except:
        devhub = None
   
    if devhub:
        env['SFDX_HUB_USERNAME'] = devhub
    else: 
        click.echo()
        click.echo('Enter the username MetaCI should use for JWT authentication to the Salesforce DX devhub.')
        env['SFDX_HUB_USERNAME'] = click.prompt('Username')

    # Get connected app info from CumulusCI keychain
    connected_app = config.keychain.get_service('connected_app')
    env['CONNECTED_APP_CALLBACK_URL'] = connected_app.callback_url
    env['CONNECTED_APP_CLIENT_ID'] = connected_app.client_id
    env['CONNECTED_APP_CLIENT_SECRET'] = connected_app.client_secret

    # Set the site url
    env['SITE_URL'] = 'https://{}.herokuapp.com'.format(payload['app']['name'])

    # Get Github credentials from CumulusCI keychain
    github = config.keychain.get_service('github')
    env['GITHUB_USERNAME'] = github.username
    env['GITHUB_PASSWORD'] = github.password
    env['GITHUB_WEBHOOK_BASE_URL'] = '{SITE_URL}/webhook/github'.format(**env)
    env['FROM_EMAIL'] = github.email

    # Prepare the payload 
    payload['overrides'] = {
        'env': env,
    } 

    # Create the new Heroku App
    headers = {
        'Accept': 'application/vnd.heroku+json; version=3',
        'Authorization': 'Bearer {}'.format(token),
    }
    resp = requests.post('https://api.heroku.com/app-setups', json=payload, headers=headers)
    if resp.status_code != 202:
        raise click.ClickException('Failed to create Heroku App.  Reponse code [{}]: {}'.format(resp.status_code, resp.json()))
    app_setup = resp.json()
    
    # Check status
    status = app_setup['status']
    click.echo()
    click.echo('Status: {}'.format(status))
    click.echo(click.style('Creating app:', fg='yellow'))
    i = 1
    build_started = False
    with click.progressbar(length=200) as bar:
        while status in ['pending']:
            check_resp = requests.get(
                'https://api.heroku.com/app-setups/{id}'.format(**app_setup),
                headers=headers,
            )
            if check_resp.status_code != 200:
                raise click.ClickException('Failed to check status of app creation.  Reponse code [{}]: {}'.format(check_resp.status_code, check_resp.json()))
            check_data = check_resp.json()

            # Stream the build log output once the build starts
            if not build_started and check_data['build'] != None:
                build_started = True
                bar.update(100)
                click.echo()
                click.echo()
                click.echo(click.style('Build {id} Started:'.format(**check_data['build']), fg='yellow'))
                build_stream = requests.get(check_data['build']['output_stream_url'], stream=True, headers=headers)
                for chunk in build_stream.iter_content():
                    click.echo(chunk, nl=False)

            # If app-setup is still pending, sleep and then poll again
            if check_data['status'] != 'pending':
                bar.update(100)
                break
            if i < 98:
                i += 1
                bar.update(i)
            time.sleep(2)

    click.echo()
    # Success
    if check_data['status'] == 'succeeded':
        click.echo(click.style('Heroku app creation succeeded!', fg='green', bold=True))
        render_recursive(check_data)
    # Failed
    elif check_data['status'] == 'failed':
        click.echo(click.style('Heroku app creation failed', fg='red', bold=True))
        render_recursive(check_data)
        if check_data['build']:
            click.echo()
            click.echo('Build Info:')
            resp = requests.get('https://api.heroku.com/builds/{id}'.format(**check_data['build']), headers=headers)
            render_recursive(resp.json())
        return
    else:
        raise click.ClickException('Received an unknown status from the Heroku app-setups API.  Full API response: {}'.format(check_data))

    heroku_app = heroku_api.app(check_data['app']['id'])

    # Apply the app shape
    click.echo()
    click.echo(click.style('# Applying App Shape', bold=True, fg='blue'))
    set_app_shape(heroku_app, app_shape, num_workers)

    click.echo()
    click.echo(click.style('# Create Admin User', bold=True, fg='blue'))
    click.echo('You will need an initial admin user to start configuring your new MetaCI site.  Enter a password for the admin user and one will be created for you')
    password = click.prompt('Password', hide_input=True, confirmation_prompt=True)
    

    # Create the admin user
    click.echo()
    click.echo(click.style('Creating admin user:'******'yellow'))
    command = 'python manage.py autoadminuser {FROM_EMAIL}'.format(**env)
    admin_output, dyno = heroku_app.run_command(command, printout=True, env={'ADMINUSER_PASS': password})
    click.echo(admin_output.splitlines()[-1:])
    
    # Create the admin user's API token
    click.echo()
    click.echo(click.style('Generating API token for admin user:'******'yellow'))
    command = 'python manage.py usertoken admin'
    token_output, dyno = heroku_app.run_command(command)
    for line in token_output.splitlines():
        if line.startswith('Token: '):
            api_token = line.replace('Token: ', '', 1).strip()

    # Create the scheduled jobs needed by MetaCI
    click.echo()
    click.echo(click.style('Creating admin user:'******'yellow'))
    command = 'python manage.py metaci_scheduled_jobs'
    admin_output, dyno = heroku_app.run_command(command, printout=True, env={'ADMINUSER_PASS': password})
    click.echo(admin_output.splitlines()[-1:])
    
    if api_token:
        service = ServiceConfig({
            'url': env['SITE_URL'],
            'token': api_token,
            'app_name': check_data['app']['name'],
        })
        config.keychain.set_service('metaci', service)
        click.echo(click.style('Successfully connected metaci to the new site at {SITE_URL}'.format(**env), fg='green', bold=True))
    else:
        click.echo(click.style('Failed to create API connection to new metaci site at {SITE_URL}.  Try metaci site connect'.format(**env), fg='red', bold=True))