
1. 项目概述这不是一个“拿来即用”的工具包而是一套面向现代云原生交付链路的工程化接口协议你搜“harness-sdk”时大概率会撞上一堆零散的 GitHub 仓库、几行 CLI 命令截图、或者某篇博客里一句“我们集成了 Harness SDK”。但没人告诉你——它根本不是传统意义上那种封装好 HTTP 请求、扔个 token 就能调的“SDK”。它是一套由 Harness 官方定义、多语言协同演进、深度绑定其 SaaS 平台语义的契约式开发套件Contract-First Development Kit。核心关键词“harness-sdk”、“Python”、“TypeScript”、“SDK”、“CLI”表面看是技术栈罗列实则揭示了它的三层存在形态底层是 REST/GraphQL 协议契约中间层是语言生成器Generator最上层才是开发者每天打交道的 Python 模块、TS 类型库和 CLI 工具。我第一次在客户现场部署 CI/CD 流水线时就栽在这点上直接 pip install harness-sdk写完代码一跑报错AttributeError: Client object has no attribute create_pipeline——后来才发现那个方法只存在于 v2.3.0 的 Python SDK 中而客户环境锁死在 v1.8.7因为他们的 Harness SaaS 实例版本是 1.29.xAPI 路径还没开放 Pipeline V2。这背后不是版本号对不上而是平台版本、API 版本、SDK 版本、CLI 版本四者构成一个强耦合矩阵缺一不可。所以如果你正打算用它做自动化部署、策略审计、或是把 Harness 状态同步到内部 CMDB这篇文章就是为你写的。它不教你怎么“安装”而是带你搞懂为什么必须用harness-cli初始化项目为什么 TypeScript 项目里 import 的类型文件实际来自harnessio/platform-client而不是harness-sdk为什么 Python SDK 的harness_platform_connector模块里Connector类的__init__方法参数列表和官方文档里写的完全不一样答案全在 SDK 的生成逻辑里。这篇文章就是把这套“黑盒生成机制”彻底拆开给你看。2. 核心设计逻辑与架构分层从契约到代码的四层生成链2.1 为什么不能“手写 SDK”——Harness 平台的 API 演进哲学Harness 不是静态 API 服务。它的后端是典型的微服务架构每个域Platform、CD、CI、Feature Flags、Security都有独立的 API Gateway 和版本控制策略。比如 Platform 域的/api/v2/connectors接口在 1.28 版本返回的是connectorType: K8sCluster到了 1.32 版本字段名升级为type: K8S_CLUSTER且新增了spec.kubernetes.clusterUrl字段。如果 SDK 是手动维护的那每次平台升级所有语言的 SDK 都得人工改一遍字段映射、重测所有单元测试、再发版——这在 Harness 这种月度大版本迭代的节奏下根本不可行。所以他们选择了 OpenAPI 3.0 Protocol Buffers 双轨制契约驱动。OpenAPI 用于描述 RESTful 接口主要是管理类操作如创建 Pipeline、更新 ConnectorProtobuf 用于 gRPC 通道主要用于高吞吐状态同步如实时获取 Execution Logs。而harness-sdk这个名字其实是整个生成体系的统称它包含三个物理上分离、逻辑上统一的产物CLI 层harness-cli负责拉取最新契约、触发本地代码生成、管理环境配置语言运行时层如 python-harness-sdk、harnessio/platform-client由 CLI 生成不托管在 PyPI/npm 上而是通过harness-cli generate命令本地产出契约层openapi.yaml / harness-platform.proto托管在 Harness 内部 Git 仓库仅对认证用户开放普通用户无法直接访问原始文件。提示你永远找不到一个叫harness-sdk的 PyPI 包。pip install harness-sdk实际安装的是harness-cli它本身不提供任何业务逻辑只是一个“生成器启动器”。2.2 四层生成链详解从 YAML 到 import 语句的完整路径整个 SDK 的诞生是一条严格顺序执行的流水线任何一层出错下游就全崩。我画过三张白板图才理清这个流程现在把它还原成可复现的步骤第一层契约源Source of TruthHarness 工程团队每天凌晨 3 点会将当天所有微服务的 OpenAPI spec 合并成一个openapi.yaml文件并编译出对应的 Protobuf.proto文件。这个文件不是公开的但harness-cli在执行generate时会通过你的 Harness Account ID 和 API Key向https://app.harness.io/gateway/api/openapi/spec发起认证请求动态拉取你所在租户Account所允许访问的最小化契约子集。这意味着A 公司的openapi.yaml里可能有featureflags模块B 公司没有开通 Feature Flags 订阅他们的契约里就压根不存在这个路径。第二层CLI 解析与裁剪harness-cliharness-cli generate --language python --output ./sdk执行时CLI 做三件事解析openapi.yaml识别所有x-harness-domain: platform的路径过滤掉 CD/CI 域根据你当前登录的 Harness Account 的 RBAC 权限剔除你无权调用的 endpoint例如DELETE /api/v2/pipelines/{identifier}如果你的角色没删 Pipeline 权限该路径会被移除将剩余的 OpenAPI schema 转换为内部 AST抽象语法树准备喂给语言生成器。第三层语言生成器Generator Engine这是最易被误解的部分。很多人以为harness-cli直接用 Jinja2 模板拼 Python 代码其实不是。它调用的是一个独立的generator-engine二进制Go 编写该引擎接收 AST然后对 Python生成pydantic模型类BaseModel子类、httpx.AsyncClient封装的ApiClient、以及按 tag 分组的 service 模块如connectors.py,pipelines.py对 TypeScript生成zodschema、fetch封装的ApiClient、以及基于harnessio/platform-client的命名空间模块关键点所有生成代码都带# Generated by harness-cli v2.4.1 on 2024-06-15T08:22:34Z注释且禁止手动修改——因为下次generate会直接覆盖。第四层运行时绑定Runtime Binding生成的 Python SDK 里harness_platform_connector.Connector类的__init__方法参数列表完全取决于openapi.yaml中#/components/schemas/Connector的required字段。如果契约里写required: [name, identifier, type]生成器就强制这三个参数为__init__的位置参数如果改成required: [name, identifier]type就变成 keyword-only 参数。这就是为什么你看到文档和代码对不上——文档是平台侧写的“理想契约”而你本地生成的 SDK是“你租户实际可用的契约”。2.3 CLI、Python SDK、TS SDK 的分工边界与协作范式很多团队试图混用 CLI 和 SDK结果踩坑。这里必须划清三条线组件主要职责典型使用场景是否可编程调用harness-cli契约拉取、代码生成、环境配置、基础命令如harness-cli login,harness-cli get accountCI/CD 流水线初始化、本地开发环境搭建✅ 可以subprocess.run([harness-cli, generate])Python SDKharness_platform_*封装 HTTP Client、提供类型安全的 domain model、处理 auth token 自动刷新自动化脚本、内部运维工具、与 Ansible/Terraform 集成✅ 完整 Python APITypeScript SDKharnessio/platform-client提供 Zod 验证、React Hook 封装useConnectorList、Vite 插件支持前端管理控制台、内部 Portal 开发、低代码平台集成✅ 完整 TS API注意harness-cli本身不提供任何业务逻辑函数。它没有create_connector()这样的方法。所有业务操作必须通过生成的 SDK 来完成。CLI 只是“产科医生”SDK 才是“新生儿”。3. 实操全流程从零开始生成并验证一个可用的 Python SDK3.1 环境准备与 CLI 安装避开 npm/pip 源污染陷阱别急着pip install harness-cli。先确认你的 Python 和 Node.js 环境是否干净。我见过太多案例公司内网 pip 源镜像了 PyPI但镜像规则没配好导致harness-cli安装时把click依赖降级到 7.x而 CLI 最低要求click8.1.0结果harness-cli --version直接报ImportError: cannot import name get_current_context。Node.js 同理nvm use 18是底线因为 CLI 的 generator-engine 二进制是用 Go 1.21 编译的而 Go 1.21 的 CGO 依赖 Node.js 18 的 OpenSSL 版本。正确步骤创建隔离环境# Python 侧 python -m venv .harness-env source .harness-env/bin/activate # Linux/macOS # .harness-env\Scripts\activate # Windows pip install --upgrade pip setuptools wheel # Node.js 侧确保 nvm 已安装 nvm install 18 nvm use 18 npm config set registry https://registry.npmjs.org/安装 CLI关键指定版本# 必须指定版本最新版不一定兼容你的 Harness 平台 pip install harness-cli2.4.1 # 验证 harness-cli --version # 输出应为 harness-cli v2.4.1登录 Harness注意不是个人账号密码而是 API Key# 在 Harness UI 右上角头像 → Account Settings → Access Tokens → Create Token # 复制生成的 Token形如 pat.abc123... harness-cli login --token pat.abc123... --account your-account-id --endpoint https://app.harness.io提示--account参数必须是你在 Harness URL 里的 Account ID不是 Account Name。比如 URL 是https://app.harness.io/ng/#/account/your-account-id/...那么your-account-id就是此处值。填错会导致harness-cli generate报403 Forbidden。3.2 生成 SDK理解--domain和--tag的真实含义harness-cli generate命令的参数不是随便选的。--domain决定生成哪个微服务的 SDK--tag决定生成哪些功能模块。常见误区是认为--domain platform就能生成所有 Platform 功能其实不然。执行生成命令# 创建输出目录 mkdir -p ./generated-sdk/python # 生成 Platform 域的 Connector 和 Pipeline 模块最常用组合 harness-cli generate \ --language python \ --output ./generated-sdk/python \ --domain platform \ --tag connectors \ --tag pipelines \ --verbose参数解析--domain platform对应 Harness 的 Platform 微服务提供账户、项目、组织、Connector、Pipeline 等基础资源管理--tag connectors只生成Connectors相关的 API 和模型POST /api/v2/connectors,GET /api/v2/connectors/{identifier}等--tag pipelines只生成Pipelines相关的 API注意不是PipelineStages或PipelineExecutions那是 CD 域的--verbose输出详细日志能看到 CLI 实际拉取的契约 URL、生成的文件列表、AST 解析耗时。生成完成后目录结构如下./generated-sdk/python/ ├── __init__.py ├── api/ │ ├── __init__.py │ ├── connectors_api.py # Connector CRUD 操作 │ └── pipelines_api.py # Pipeline CRUD 操作 ├── models/ │ ├── __init__.py │ ├── connector.py # Connector 模型含 type, name, identifier 等字段 │ └── pipeline.py # Pipeline 模型含 name, identifier, stages 等字段 └── api_client.py # 统一的 AsyncClient 封装关键验证点检查models/connector.py中Connector类的__init__方法签名class Connector(BaseModel): name: str identifier: str type: str # 这个字段必须存在且是 required # ... 其他字段如果type字段缺失说明你租户的契约里Connector的required列表没包含它——这通常意味着你的 Harness 版本低于 1.30或者你的 Account 没开通 K8s Cluster Connector 订阅。3.3 编写第一个可用脚本创建一个 Kubernetes Connector生成 SDK 后立刻写个脚本验证。别用print(Hello World)直接调 API。脚本create_k8s_connector.pyimport asyncio from generated_sdk.python.api.connectors_api import ConnectorsApi from generated_sdk.python.models.connector import Connector from generated_sdk.python.api_client import ApiClient async def main(): # 1. 初始化 API Client自动读取 harness-cli login 的配置 async with ApiClient() as client: # 2. 创建 Connector 实例字段必须严格匹配生成的模型 connector Connector( namemy-k8s-cluster, identifiermy-k8s-cluster-id, typeK8S_CLUSTER, # 注意必须大写且与契约一致 descriptionProduction cluster in AWS EKS, # spec 字段是嵌套模型必须用 dict 或子模型 spec{ kubernetes: { clusterUrl: https://E1234567890123456789.gr7.us-east-1.eks.amazonaws.com, auth: { type: DELEGATED, delegateSelectors: [default] } } } ) # 3. 调用 API注意create_connector 是异步方法 api ConnectorsApi(client) try: response await api.create_connector( bodyconnector, account_identifieryour-account-id, # 必须显式传入 org_identifierdefault, # 默认组织 project_identifierdefault # 默认项目 ) print(f✅ Connector created: {response.identifier}) except Exception as e: print(f❌ Failed: {e}) if __name__ __main__: asyncio.run(main())执行与调试python create_k8s_connector.py如果失败按此顺序排查检查account_identifier是否和harness-cli login时的--account一致检查org_identifier和project_identifier是否真实存在可在 Harness UI 的左上角切换检查spec字段结构是否与models/connector.py中Connector.spec的类型定义匹配spec: Optional[Dict[str, Any]] None表示接受 dict但内容必须符合契约查看harness-cli login生成的~/.harness/config.json确认token未过期有效期默认 30 天。3.4 TypeScript SDK 生成与 React 集成为什么harnessio/platform-client是“半成品”TypeScript 的生成逻辑类似但产物形态不同。执行mkdir -p ./generated-sdk/ts harness-cli generate --language typescript --output ./generated-sdk/ts --domain platform --tag connectors生成的./generated-sdk/ts/index.ts会导出export * from ./api/connectors-api; export * from ./models/connector; export * from ./api-client;但你会发现./generated-sdk/ts/api/connectors-api.ts里createConnector方法返回的是PromiseConnector而不是PromiseAxiosResponseConnector。这是因为 SDK 内部封装了fetch并做了错误分类400-499→ApiError含status,statusText,body500→ApiNetworkError网络层错误成功 → 直接解包 JSON 返回Connector实例。在 React 中安全使用import { useQuery } from tanstack/react-query; import { ConnectorsApi, Connector } from ./generated-sdk/ts; const ConnectorsList () { const { data, isLoading, error } useQueryConnector[], Error({ queryKey: [connectors], queryFn: async () { const api new ConnectorsApi(); // 注意TS SDK 不需要显式传 account/org/project它从全局 context 读取 const response await api.listConnectors({ accountIdentifier: your-account-id, orgIdentifier: default, projectIdentifier: default }); return response.content; // content 是 Connector[] 数组 } }); if (isLoading) return divLoading.../div; if (error) return divError: {error.message}/div; return ( ul {data?.map(conn ( li key{conn.identifier}{conn.name} ({conn.type})/li ))} /ul ); };注意harnessio/platform-client是一个 npm 包但它不包含任何生成代码。它只是提供ApiClient基类、ApiError类型、以及fetch封装。真正的业务 APIConnectorsApi和模型Connector必须由harness-cli generate产出并手动 import。这是为了确保类型安全——npm 包版本和你生成的契约版本必须严格对齐。4. 常见问题与避坑指南那些官方文档绝不会告诉你的细节4.1 “ModuleNotFoundError: No module named harness_platform_connector” —— 生成路径与 Python Path 的战争这是新手最高频报错。原因很简单harness-cli generate生成的 SDK 是纯本地文件Python 解释器默认不认识generated-sdk/python这个路径。你不能import harness_platform_connector因为harness_platform_connector不是包名而是生成目录下的harness_platform_connector模块名。正确做法只有两种临时添加路径开发阶段import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent / generated-sdk / python)) # 然后才能 from harness_platform_connector import Connector安装为可编辑包生产推荐在./generated-sdk/python目录下创建setup.pyfrom setuptools import setup, find_packages setup( nameharness-platform-sdk, version1.0.0, packagesfind_packages(), install_requires[httpx0.24.0, pydantic2.0.0], )然后执行cd ./generated-sdk/python pip install -e .之后就能import harness_platform_connector了。警告不要pip install -e .在项目根目录必须在generated-sdk/python目录下执行否则find_packages()会扫描整个项目把tests/、docs/都打包进去。4.2 TypeScript 类型“不生效”Zod Schema 与 IDE 的隐式契约生成的 TS SDK 里Connector模型是用 Zod 定义的export const ConnectorSchema z.object({ name: z.string(), identifier: z.string(), type: z.enum([K8S_CLUSTER, AWS_CLOUD_PROVIDER, GCP_CLOUD_PROVIDER]), // ... }); export type Connector z.infertypeof ConnectorSchema;但 VS Code 有时不提示字段或提示Property spec does not exist on type Connector。这是因为 Zod 的infer类型在某些 TS 版本下IDE 无法完美推导嵌套对象。终极解决方案确保tsconfig.json中compilerOptions启用{ compilerOptions: { skipLibCheck: true, strict: true, moduleResolution: node } }在ConnectorSchema定义后手动添加类型断言// 在 generated-sdk/ts/models/connector.ts 底部追加 export interface Connector extends z.infertypeof ConnectorSchema {}这样 IDE 就能正确识别所有字段了。4.3 CLI 生成失败“Failed to fetch OpenAPI spec” —— 网络代理与证书的双重绞杀企业内网环境下harness-cli generate经常卡在Fetching OpenAPI spec...。这不是 CLI 的 bug而是你的网络策略在作祟。排查步骤检查是否设置了系统代理echo $HTTP_PROXY $HTTPS_PROXY # 如果有输出CLI 会自动使用但可能指向错误的 proxy临时禁用代理unset HTTP_PROXY HTTPS_PROXY harness-cli generate --verbose如果仍失败检查 SSL 证书# 测试能否 curl 通 Harness 端点 curl -v https://app.harness.io/gateway/api/openapi/spec?accountIdentifieryour-account-id如果报SSL certificate problem: unable to get local issuer certificate说明你的公司 CA 证书没被 Python/Node.js 信任。解决方案将公司根证书添加到REQUESTS_CA_BUNDLE环境变量或在harness-cli源码里~/.local/share/harness-cli/修改config.json添加insecure: true仅测试环境生产禁用。4.4 Python SDK 的AsyncClient为何不支持同步调用—— 异步 I/O 的硬性约束harness-cli生成的 Python SDK 全部基于httpx.AsyncClient没有同步版本。这不是设计缺陷而是 Harness API 的 SLA 要求单次请求 P95 200ms而同步requests在高并发下容易阻塞线程池。所以你不能写# ❌ 错误没有 sync 方法 client ApiClient() response client.create_connector(...) # 会报 AttributeError必须用 async/awaitimport asyncio from generated_sdk.python.api.connectors_api import ConnectorsApi async def create(): async with ApiClient() as client: api ConnectorsApi(client) return await api.create_connector(...) # 在同步上下文中调用 result asyncio.run(create()) # 仅适用于脚本主入口 # 或在 FastAPI 中直接用 async def实操心得如果你的项目是 Django同步框架别硬套asyncio.run()。应该用httpx.Client手动构造请求或用django-q等异步任务队列来包裹 SDK 调用。4.5 “The current configured Flutter SDK is not known to be fully supported” —— 无关错误的干扰源搜索harness-sdk时你会频繁看到这条 Flutter 相关错误。它和 Harness SDK完全无关。这是 Flutter CLI 在检查本地 SDK 版本时发现你安装的 Flutter 版本不在其白名单内比如你用了 dev channel 的 3.22.0-0.0.pre于是抛出警告。解决方法flutter upgrade # 升级到 stable channel # 或 flutter config --no-analytics # 关闭检查不推荐别被这个错误误导去查 Harness 文档——它只是 SEO 垃圾信息源于开发者同时安装了 Flutter 和 Harness CLI日志混在一起了。5. 进阶技巧与生产实践让 SDK 真正融入你的工程体系5.1 CI/CD 流水线中自动生成 SDK避免“本地生成提交代码”的反模式很多团队把生成的 SDK 提交到 Git这是危险的。因为 SDK 是“活”的它随 Harness 平台升级而变。正确的做法是在 CI 流水线中每次构建时动态生成 SDK。GitHub Actions 示例.github/workflows/sdk-generate.ymlname: Generate Harness SDK on: push: paths: - infrastructure/** - .harness-config.yml jobs: generate-sdk: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install harness-cli run: pip install harness-cli2.4.1 - name: Login to Harness env: HARNESS_TOKEN: ${{ secrets.HARNESS_TOKEN }} run: | harness-cli login \ --token $HARNESS_TOKEN \ --account ${{ secrets.HARNESS_ACCOUNT_ID }} \ --endpoint https://app.harness.io - name: Generate Python SDK run: | mkdir -p ./sdk/python harness-cli generate \ --language python \ --output ./sdk/python \ --domain platform \ --tag connectors \ --tag pipelines - name: Cache SDK uses: actions/cachev3 with: path: ./sdk/python key: sdk-python-${{ hashFiles(**/.harness-config.yml) }} - name: Commit SDK changes uses: stefanzweifel/git-auto-commit-actionv4 with: commit_message: chore(sdk): auto-generate Python SDK file_path: sdk/python这样每次 Harness 平台升级只要 CI 触发SDK 就自动更新代码永远和平台保持一致。5.2 SDK 版本锁定与契约快照应对平台升级的“熔断”策略生产环境不能容忍 SDK 突然变更。你需要一种机制在 Harness 平台升级时先冻结 SDK等测试通过后再放开。方案契约快照OpenAPI SnapshotHarness CLI 支持导出当前契约harness-cli openapi export --output ./snapshots/openapi-1.32.0.yaml然后在 CI 中用这个快照生成 SDKharness-cli generate --openapi ./snapshots/openapi-1.32.0.yaml --language python ...这样即使 Harness 升级到 1.33你的 SDK 依然基于 1.32 的契约直到你手动更新快照。5.3 自定义生成模板当默认 SDK 不满足你的架构需求harness-cli允许注入自定义 Jinja2 模板。比如你的团队强制要求所有 API 调用必须记录 trace_id你可以在templates/python/api_client.j2里修改class ApiClient: def __init__(self, ...): self._client httpx.AsyncClient(...) # 添加全局 header self._client.headers.update({X-Trace-ID: str(uuid.uuid4())})然后执行harness-cli generate --template-dir ./my-templates --language python ...这比 fork 官方 SDK 并维护分支成本低得多。5.4 监控 SDK 健康度用契约 diff 发现潜在断裂点最后也是最重要的经验不要等线上报错才发现问题。建立每日契约 diff 监控。写一个脚本每天凌晨拉取最新openapi.yaml和昨天的做 diff# 比较两个 YAML 文件的 paths 差异 yq eval paths | select(length 3) | join(.) openapi-2024-06-14.yaml paths-14.txt yq eval paths | select(length 3) | join(.) openapi-2024-06-15.yaml paths-15.txt diff paths-14.txt paths-15.txt | grep ^ | sed s/^ // new-endpoints.txt如果new-endpoints.txt有内容就说明平台新增了 API你的 SDK 可能需要重新生成。把这个 diff 结果发到 Slack 频道让团队提前知晓。我在上一家公司就靠这套机制提前 3 天发现 Harness 移除了DELETE /api/v2/secret-managers接口及时重构了密钥轮换逻辑避免了上线当天的 P0 故障。技术债不是欠出来的是没看见才欠的。而harness-sdk的价值正在于它把“看不见的契约变化”变成了“看得见的代码变更”。