Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skillable Lab setup Python script #287

Merged
merged 33 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
870e4e1
lab_setup first draft
cedricvidal Dec 7, 2024
938e235
Add an optional --tenant param for the az step
cedricvidal Dec 7, 2024
b12981d
Using rich-click
cedricvidal Dec 7, 2024
2f8d7ed
Steps as functions
cedricvidal Dec 7, 2024
22dc3f9
Each step function takes context variables as kw parameters
cedricvidal Dec 7, 2024
f07c0fc
if a step returns a dict then merge it into the params
cedricvidal Dec 7, 2024
d2d85ff
username and password optional to ease dev in normal tenant
cedricvidal Dec 7, 2024
f0ada69
GitHub Authentication step
cedricvidal Dec 7, 2024
aff75fd
Fixed step number
cedricvidal Dec 7, 2024
e5f8738
Display total steps
cedricvidal Dec 7, 2024
65bdf6d
Fork repo step
cedricvidal Dec 7, 2024
2e4e0f6
Skip Github auth if already authenticated
cedricvidal Dec 7, 2024
4741c9e
not need to call gh auth status if force is true
cedricvidal Dec 7, 2024
459ea22
Skip Azure CLI authentication if we're already authenticated
cedricvidal Dec 7, 2024
d73bbd8
Force Azure CLI authentication if force is true
cedricvidal Dec 7, 2024
c58490a
--force
cedricvidal Dec 7, 2024
7f7d105
Azd auth step
cedricvidal Dec 7, 2024
7dadc0d
Insist not to use personal credentials for the azd auth step
cedricvidal Dec 7, 2024
62f8a22
Only create the new azd env if it doesn't exist already
cedricvidal Dec 7, 2024
ef4a12b
resolve the .env file relatively to the script
cedricvidal Dec 7, 2024
a9c30f8
resolve the roles.sh script relatively to the current script
cedricvidal Dec 7, 2024
f577190
Fix bang comment
cedricvidal Dec 7, 2024
84731d5
Update lab manual
cedricvidal Dec 7, 2024
e50c4f2
Only fork repo if we don't already have a remote called upstream
cedricvidal Dec 8, 2024
3065938
Removed azd auth status check, not reliable
cedricvidal Dec 8, 2024
b3e2c71
Save step progress
cedricvidal Dec 8, 2024
d984c81
Display a message when all steps have already been executed
cedricvidal Dec 8, 2024
ac86696
Add a --step param that allows to set which step to resume from
cedricvidal Dec 8, 2024
f0f9acf
Hightlight step number
cedricvidal Dec 8, 2024
6ab1809
Updated docstring
cedricvidal Dec 8, 2024
1cd26f6
Better azd login instructions
cedricvidal Dec 8, 2024
4d36b58
More styling
cedricvidal Dec 8, 2024
b32bf78
Update file reference in LAB-MANUAL.md
marlenezw Dec 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions docs/workshop/LAB-MANUAL.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
## Azure Credentials:

# CREDENTIALS

++Username = "@lab.CloudPortalCredential(User1).Username"
Password = "@lab.CloudPortalCredential(User1).Password"
AzureEnvName = "[email protected]"
Subscription = "@lab.CloudSubscription.Id"++


**If you are viewing this from the Skillable lab page** the above are your unique azure credentials.

> **Note**: You will be asked to copy the above block in the lab later so keep this information readily available.

**If you are viewing this from Github:** The above are not your credentials. They are placeholders. Your actual credentials can be seen on the Skillable lab page.

***

### Welcome to this Microsoft workshop!
### Welcome to the AI Tour and workshop WRK551!

In this session, you will learn how to build the app, **Contoso Creative Writer**. This app will assist the marketing team at Contoso Outdoors in creating trendy, well-researched articles to promote the company’s products.

Expand All @@ -43,9 +25,26 @@ To participate in this workshop, you will need:
3. Click the green **<> Create codespace** button at the bottom of the page.
* This will open a pre-built Codespace on main.

4. Once your Codespace is ready:
> **🚧 IMPORTANT**: Do not open the GitHub Codespace on a fork of the repository, this would prevent you from using the prebuilt Codespace container image. Don't worry, you'll have the possibility to fork the repository later.

4. Once your Codespace is ready, **run the following command**:

```
./docs/workshop/lab_setup.py \
--username "@lab.CloudPortalCredential(User1).Username" \
--password "@lab.CloudPortalCredential(User1).Password" \
--azure-env-name "[email protected]" \
--subscription "@lab.CloudSubscription.Id"
```

> [!IMPORTANT]
> - **If you are viewing this from the Skillable lab page**: The above are your unique azure credentials.
> - **If you are viewing this from Github**: The above are not your credentials. They are placeholders. Your actual credentials can be seen on the Skillable lab page.


5. Once the previous script is complete:
* In the file explorer look for the **docs** folder and in it open the **workshop** folder.
* Open the **LAB-SETUP.ipynb** file.
* Open the **workshop-1-intro.ipynb** file.
* Follow the instructions to get going!

Have fun building!🎉
256 changes: 256 additions & 0 deletions docs/workshop/lab_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
#!/usr/bin/env python

import rich_click as click
import subprocess
import os
from functools import wraps
from typing import List, Callable
from click import style
from pathlib import Path
from inspect import signature

# Add these constants near the top
TEMP_FILE = Path.home() / '.lab_setup_progress'

# Step registration
steps: List[tuple[Callable, str]] = []

def blue(text: str):
return style(text, fg="blue")

def bold(text: str):
return style(text, fg="bright_white", bold=True)

def step(label: str):

"""Decorator to register and label setup steps"""
def decorator(func):
@wraps(func)
def wrapper(*args, step_number, total_steps, **kwargs):
click.echo(f"\n{bold(f'Step {step_number}/{total_steps}')}: {blue(label)}")
click.echo()
return func(*args, **kwargs)
steps.append((wrapper, label))
return wrapper
return decorator

@step("GitHub Authentication")
def github_auth(*, force: bool = False):
"""Authenticate with GitHub using the gh CLI tool"""
# Only check authentication status if not forcing re-auth
if not force:
result = subprocess.run(['gh', 'auth', 'status'],
capture_output=True,
text=True,
check=False)
if result.returncode == 0:
click.echo("Already authenticated with GitHub")
return

# Proceed with authentication
process = subprocess.Popen(
['gh', 'auth', 'login',
'--hostname', 'github.com',
'--git-protocol', 'https',
'--web',
'--scopes', 'workflow'],
stdin=subprocess.PIPE,
env={**os.environ, 'GITHUB_TOKEN': ''},
text=True
)
process.communicate(input='Y\n')

@step("Fork GitHub Repository")
def fork_repository():
"""Fork the current repository using the gh CLI tool"""
# Check if upstream remote already exists
result = subprocess.run(['git', 'remote', 'get-url', 'upstream'],
capture_output=True,
text=True,
check=False)
if result.returncode == 0:
click.echo("Repository already has an upstream remote")
return

# Proceed with fork if no upstream remote exists
subprocess.run(['gh', 'repo', 'fork', '--remote'], check=True)

@step("Azure CLI Authentication")
def azure_login(*, username: str = None, password: str = None, tenant: str = None, force: bool = False):
# Only check authentication status if not forcing re-auth
if not force:
result = subprocess.run(['az', 'account', 'show'],
capture_output=True,
text=True,
check=False)
if result.returncode == 0:
click.echo("Already authenticated with Azure CLI")
return

# Proceed with login if not authenticated or force=True
login_cmd = ['az', 'login']
if username and password:
login_cmd.extend(['-u', username, '-p', password])
if tenant:
login_cmd.extend(['--tenant', tenant])
subprocess.run(login_cmd, check=True)

@step("Azure Developer CLI Authentication")
def azd_login(*, username: str = None, password: str = None, tenant: str = None, force: bool = False):
"""Authenticate with Azure Developer CLI using device code"""

# Display credentials if provided
if username and password:
opts = {'underline': True}
click.echo(f"{style('When asked to ', **opts)}{style('Pick an account', **opts, bold=True)}{style(', hit the ', **opts)}{style('Use another account', **opts, bold=True)}{style(' button and enter the following:', **opts)}")
click.echo(f"Username: {style(username, fg='blue', bold=True)}")
click.echo(f"Password: {style(password, fg='blue', bold=True)}")
click.echo()
click.echo(f"{style('IMPORTANT', fg='red', reverse=True)}: {style('DO NOT use your personal credentials for this step!', fg='red', underline=True)}")
click.echo()

# Proceed with authentication
login_cmd = ['azd', 'auth', 'login', '--use-device-code', '--no-prompt']
if tenant:
login_cmd.extend(['--tenant-id', tenant])
subprocess.run(login_cmd, check=True)

@step("Azure Developer CLI Environment Setup")
def create_azd_environment(*, azure_env_name: str, subscription: str):
# Check if environment already exists
result = subprocess.run(
['azd', 'env', 'list'],
capture_output=True,
text=True,
check=True
)

if azure_env_name in result.stdout:
click.echo(f"Environment '{azure_env_name}' already exists")
return

# Create new environment if it doesn't exist
azd_cmd = [
'azd', 'env', 'new', azure_env_name,
'--location', 'canadaeast',
'--subscription', subscription
]
subprocess.run(azd_cmd, check=True)

@step("Refresh AZD Environment")
def refresh_environment(*, azure_env_name: str):
subprocess.run([
'azd', 'env', 'refresh',
'-e', azure_env_name,
'--no-prompt'
], check=True)

@step("Export Environment Variables")
def export_variables():
# Get the directory where the script is located and resolve .env path
env_path = Path(__file__).parent.parent.parent / '.env'

with open(env_path, 'w') as env_file:
subprocess.run(['azd', 'env', 'get-values'], stdout=env_file, check=True)

@step("Run Roles Script")
def run_roles():
# Get the directory where the script is located
script_dir = Path(__file__).parent
roles_script = script_dir.parent.parent / 'infra' / 'hooks' / 'roles.sh'
subprocess.run(['bash', str(roles_script)], check=True)

@step("Execute Postprovision Hook")
def run_postprovision(*, azure_env_name: str):
process = subprocess.Popen(
['azd', 'hooks', 'run', 'postprovision', '-e', azure_env_name],
stdin=subprocess.PIPE,
text=True
)
process.communicate(input='1\n')

@click.command()
@click.option('--username', help='Azure username/email for authentication')
@click.option('--password', help='Azure password for authentication', hide_input=True)
@click.option('--azure-env-name', required=True, help='Name for the new Azure environment')
@click.option('--subscription', required=True, help='Azure subscription ID to use')
@click.option('--tenant', help='Azure tenant ID')
@click.option('--force', is_flag=True, help='Force re-authentication and start from beginning')
@click.option('--step', type=int, help='Resume from a specific step number (1-based)')
def setup(username, password, azure_env_name, subscription, tenant, force, step):
"""
Automates Azure environment setup and configuration.

This command will:
1. GitHub Authentication
2. Fork GitHub Repository
3. Azure CLI Authentication
4. Azure Developer CLI Authentication
5. Azure Developer CLI Environment Setup
6. Refresh AZD Environment
7. Export Environment Variables
8. Run Roles Script
9. Execute Postprovision Hook
"""
try:
# Create parameters dictionary
params = {
'username': username,
'password': password,
'azure_env_name': azure_env_name,
'subscription': subscription,
'tenant': tenant,
'force': force
}

# Determine starting step
start_step = 0
if step is not None:
if not 1 <= step <= len(steps):
raise click.BadParameter(f"Step must be between 1 and {len(steps)}")
start_step = step - 1
elif not force and TEMP_FILE.exists():
start_step = int(TEMP_FILE.read_text().strip())
if start_step >= len(steps):
click.echo("\nAll steps were already successfully executed!")
click.echo("Use --force to execute all steps from the beginning if needed.")
return
click.echo(f"\nResuming from step {blue(start_step + 1)}")

# Execute all registered steps
for index, entry in enumerate(steps):
from inspect import signature
# Skip steps that were already completed
if index < start_step:
continue

step_func, _ = entry

# Get the parameter names for this function
sig = signature(step_func.__wrapped__)
# Filter params to only include what the function needs
step_params = {
name: params[name]
for name in sig.parameters
if name in params
}
# Execute step and merge any returned dict into params
result = step_func(step_number=index + 1, total_steps=len(steps), **step_params)
if isinstance(result, dict):
params.update(result)

# Save progress after each successful step
TEMP_FILE.write_text(str(index + 1))

# Clean up temp file on successful completion
if TEMP_FILE.exists():
TEMP_FILE.unlink()

click.echo("\nSetup completed successfully!")

except subprocess.CalledProcessError as e:
click.echo(f"Error during setup: {str(e)}", err=True)
raise click.Abort()

if __name__ == '__main__':
setup()
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
click
rich-click